#!/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 urllib.parse

import lib.args
import lib.base
import lib.db_sqlite
import lib.human
import lib.time
import lib.url
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Monitors network I/O and link states on all interfaces of FortiGate appliances running FortiOS via the REST API. Alerts only if bandwidth thresholds have been exceeded for a configurable number of consecutive check runs (default: 5), suppressing short spikes. Reports per-interface traffic counters, error rates, and link status."""

DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_COUNT = (
    5  # measurements; if check runs once per minute, this is a 5 minute interval
)
DEFAULT_TIMEOUT = 3
DEFAULT_WARN = int(1e9 * 0.8)  # 80% of 1'000'000'000 bit/s
DEFAULT_CRIT = int(1e9 * 0.9)  # 90% of 1'000'000'000 bit/s


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(
        '--count',
        help=lib.args.help('--count') + ' Default: %(default)s',
        dest='COUNT',
        type=int,
        default=DEFAULT_COUNT,
    )

    parser.add_argument(
        '-c',
        '--critical',
        help='CRIT threshold for link bandwidth saturation in bits per second. '
        'Applied over the last `--count` measurements. '
        'Default: %(default)s',
        dest='CRIT',
        type=int,
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '-H',
        '--hostname',
        help='FortiOS-based appliance address, optionally including port. '
        'Example: `--hostname 192.168.1.1:443`.',
        dest='HOSTNAME',
        required=True,
    )

    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='FortiOS REST API single-use access token.',
        dest='PASSWORD',
        required=True,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold for link bandwidth saturation in bits per second. '
        'Applied over the last `--count` measurements. '
        'Default: %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


def get_interface_states_from_db(conn, table, interface):
    """Fetch inventorized link state for an interface from SQLite."""
    # table is sanitized to a safe SQL identifier by __filter_str()
    table = lib.db_sqlite.__filter_str(table)
    return lib.db_sqlite.select(
        conn,
        f"""
        SELECT *
        FROM {table}
        WHERE interface = :interface
        """,  # nosec B608
        {'interface': interface},
        fetchone=True,
    )


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
    # Resource to get interface data from
    token = urllib.parse.quote(args.PASSWORD)
    url = (
        f'https://{args.HOSTNAME}/api/v2/monitor/system/'
        f'interface/select?access_token={token}'
    )
    result = lib.base.coe(
        lib.url.fetch_json(
            url, insecure=args.INSECURE, no_proxy=args.NO_PROXY, timeout=args.TIMEOUT
        )
    )

    conn = lib.base.coe(
        lib.db_sqlite.connect(
            filename='linuxfabrik-monitoring-plugins-fortios-network-io.db'
        )
    )

    # create the db inventory table for the link states - there we only
    # want to insert records once if they do not exist
    # so set a unique constraint and ignore any insert errors later on
    definition = """
            interface  TEXT NOT NULL,
            link       INT NOT NULL,
            speed      REAL NOT NULL,
            duplex     INT NOT NULL
        """
    lib.base.coe(
        lib.db_sqlite.create_table(conn, definition, table=f'state_{args.HOSTNAME}')
    )
    lib.base.coe(
        lib.db_sqlite.create_index(
            conn, column_list='interface', table=f'state_{args.HOSTNAME}', unique=True
        )
    )

    # create the db table for perfdata
    definition = """
            interface  TEXT NOT NULL,
            tx_bytes   INT NOT NULL,
            rx_bytes   INT NOT NULL,
            timestamp  INT NOT NULL
        """
    lib.base.coe(
        lib.db_sqlite.create_table(conn, definition, table=f'perfdata_{args.HOSTNAME}')
    )

    msg_link = ''
    state = STATE_OK
    timestamp = lib.time.now()

    # save the data to the database
    # and get the link/duplex/speed state of the interface
    for interface in result['results'].values():
        data = {}
        data['interface'] = interface['name']
        data['tx_bytes'] = interface['tx_bytes']
        data['rx_bytes'] = interface['rx_bytes']
        data['timestamp'] = timestamp
        lib.base.coe(
            lib.db_sqlite.insert(conn, data, table=f'perfdata_{args.HOSTNAME}')
        )

        data = {}
        data['interface'] = interface['name']
        data['link'] = int(interface.get('link', 0))
        data['speed'] = interface['speed']
        data['duplex'] = interface['duplex']
        _, first = get_interface_states_from_db(
            conn, f'state_{args.HOSTNAME}', data['interface']
        )

        if first:  # if inventorized
            # we got something from DB, so there is inventorized
            # data, so compare against current values
            # we want to be informed if
            # * link state changes from true to false
            # * speed rate changes from higher to lower value
            # * duplex mode changes from higher to lower value
            if data['link'] != first['link']:
                msg_link += (
                    f'Link state for {data["interface"]} changed '
                    f'from {first["link"]} to {data["link"]} '
                    f'(WARN). '
                )
                state = lib.base.get_worst(state, STATE_WARN)
            if data['speed'] != first['speed']:
                msg_link += (
                    f'Speed rate for {data["interface"]} changed '
                    f'from {first["speed"]} to {data["speed"]} '
                    f'(WARN). '
                )
                state = lib.base.get_worst(state, STATE_WARN)
            if data['duplex'] != first['duplex']:
                msg_link += (
                    f'Duplex mode for {data["interface"]} changed '
                    f'from {first["duplex"]} to {data["duplex"]} '
                    f'(WARN). '
                )
                state = lib.base.get_worst(state, STATE_WARN)
        else:
            # network states are not saved at all (first time),
            # so we got `False` in `inventorized`
            # save the states one time (only if not exist)
            _, _ = lib.db_sqlite.insert(conn, data, table=f'state_{args.HOSTNAME}')

    max_count = args.COUNT * len(result['results'])
    lib.base.coe(
        lib.db_sqlite.cut(conn, table=f'perfdata_{args.HOSTNAME}', _max=max_count)
    )
    lib.base.coe(lib.db_sqlite.commit(conn))

    # compute the load1 and loadn (load-n is what we want to alert about later on)
    loads = lib.base.coe(
        lib.db_sqlite.compute_load(
            conn,
            sensorcol='interface',
            datacols=['tx_bytes', 'rx_bytes'],
            count=args.COUNT,
            table=f'perfdata_{args.HOSTNAME}',
        )
    )
    if not loads:
        lib.db_sqlite.close(conn)
        lib.base.oao('Waiting for more data.', STATE_OK)

    msg_saturation = ''
    msg_header = ''
    perfdata = ''
    table_data = []
    max_load = 0

    for interface in loads:
        rx1bps = interface['rx_bytes1'] * 8
        tx1bps = interface['tx_bytes1'] * 8
        rxnbps = interface['rx_bytesn'] * 8
        txnbps = interface['tx_bytesn'] * 8
        sum1bps = rx1bps + tx1bps

        if sum1bps > max_load:
            msg_header = (
                f'{interface["interface"]}: '
                f'{lib.human.bits2human(rx1bps)}/'
                f'{lib.human.bits2human(tx1bps)}'
                f' bps (rx/tx, current).'
            )
            max_load = sum1bps

        # we want to be informed if
        # * link saturation is above a certain threshold
        #   (on duplex lines and 1 gbps alert if saturation
        #   is >= 80/90% on each rx/tx compared to loadn)
        sensor_state = lib.base.get_state(rxnbps, args.WARN, args.CRIT, _operator='ge')
        sensor_state = lib.base.get_worst(
            sensor_state,
            lib.base.get_state(txnbps, args.WARN, args.CRIT, _operator='ge'),
        )
        if sensor_state in (STATE_WARN, STATE_CRIT):
            msg_saturation += (
                f'Bandwidth saturation for '
                f'{interface["interface"]}: '
                f'{lib.human.bits2human(rxnbps)}/'
                f'{lib.human.bits2human(txnbps)}'
                f' bps (rx/tx) is too high'
                f'{lib.base.state2str(sensor_state, prefix=" (", suffix=")")}'
                f'. '
            )

        perfdata += lib.base.get_perfdata(
            f'{interface["interface"]}_rx1',
            interface['rx_bytes1'],
            uom='B',
            warn=args.WARN / 8,
            crit=args.CRIT / 8,
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            f'{interface["interface"]}_tx1',
            interface['tx_bytes1'],
            uom='B',
            warn=args.WARN / 8,
            crit=args.CRIT / 8,
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            f'{interface["interface"]}_rxn',
            interface['rx_bytesn'],
            uom='B',
            warn=args.WARN / 8,
            crit=args.CRIT / 8,
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            f'{interface["interface"]}_txn',
            interface['tx_bytesn'],
            uom='B',
            warn=args.WARN / 8,
            crit=args.CRIT / 8,
            _min=0,
        )

        table_data.append(
            {
                'interface': (
                    f'{interface["interface"]}'
                    f'{lib.base.state2str(sensor_state, prefix=" (", suffix=")")}'
                ),
                'rx1': lib.human.bits2human(rx1bps),
                'tx1': lib.human.bits2human(tx1bps),
                f'rx{args.COUNT}': lib.human.bits2human(rxnbps),
                f'tx{args.COUNT}': lib.human.bits2human(txnbps),
            }
        )
        state = lib.base.get_worst(state, sensor_state)

    # get_table(data, keys, header=None, sort_by_key=None, sort_order_reverse=False):
    table = lib.base.get_table(
        table_data,
        ['interface', 'rx1', 'tx1', f'rx{args.COUNT}', f'tx{args.COUNT}'],
        header=[
            'interface',
            'rx1bps',
            'tx1bps',
            f'rx{args.COUNT}bps',
            f'tx{args.COUNT}bps',
        ],
    )

    lib.db_sqlite.close(conn)

    # over and out
    if not msg_link and not msg_saturation:
        # no warnings, so print the interface with the highest load
        lib.base.oao(msg_header + '\n\n' + table, state, perfdata)
    else:
        lib.base.oao(msg_link + msg_saturation + '\n' + table, state, perfdata)


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