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

import lib.args  # pylint: disable=C0413
import lib.base  # pylint: disable=C0413
import lib.db_sqlite  # pylint: disable=C0413
import lib.disk  # pylint: disable=C0413

try:
    import lib.smb  # pylint: disable=C0413
    HAVE_SMB = True
except ModuleNotFoundError as e:
    HAVE_SMB = False
    missing_lib = e.name # pylint: disable=C0103
import lib.txt  # pylint: disable=C0413
import lib.url  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

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

DESCRIPTION = """This check imports a CSV file into an SQLite database and can then run a separate
                 warning query and/or a critical query against it. The result - the number of
                 items found or a specific number - can be checked against a range expression.
              """

DEFAULT_CHUNKSIZE = 1000
DEFAULT_DELIMITER = ','
DEFAULT_INSECURE = False
DEFAULT_NEWLINE = None
DEFAULT_NO_PROXY = False
DEFAULT_QUOTECHAR = '"'
DEFAULT_SKIP_HEADER = False
DEFAULT_TIMEOUT = 3


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(
        '--chunksize',
        help='Breaks up the transfer of data from the csv to the SQLite database in chunks as '
             'to not run out of memory. '
             'Default: %(default)s',
        dest='CHUNKSIZE',
        type=int,
        default=DEFAULT_CHUNKSIZE,
    )

    parser.add_argument(
        '--columns-query',
        help='Describe the columns and their datatypes using an sql statement. '
             'Example: `"col1 INTEGER PRIMARY KEY, col2 TEXT NOT NULL, col3 TEXT NOT NULL UNIQUE"`',
        dest='COLUMNS_QUERY',
        required=True,
    )

    parser.add_argument(
        '-c', '--critical',
        help='Set the CRIT threshold. Supports ranges.',
        dest='CRIT',
    )

    parser.add_argument(
        '--critical-query',
        help='`SELECT` statement. If its result contains more than one column, the number of rows '
             'is checked against `--critical`, otherwise the single value is used.',
        dest='CRITICAL_QUERY',
        default='',
    )

    parser.add_argument(
        '--delimiter',
        help='CSV delimiter. '
             'Default: `"%(default)s"`',
        dest='DELIMITER',
        default=DEFAULT_DELIMITER,
    )

    parser.add_argument(
        '--filename',
        help='Path to CSV file. This is mutually exclusive with -u / --url.',
        dest='FILENAME',
        type=str,
    )

    parser.add_argument(
        '--insecure',
        help='This option explicitly allows to perform "insecure" '
             'SSL connections. '
             'Default: %(default)s',
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--newline',
        help='CSV newline. When reading input from the CSV, if newline is `None`, universal '
             'newlines mode is enabled. Lines in the input can end in `"\n"`, `"\r"`, or `"\r\n"`, '
             'and these are translated into `"\n"` before being returned to this plugin. If it is '
             '`""`, universal newlines mode is enabled, but line endings are returned to this '
             'plugin untranslated. If it has any of the other legal values, input lines are only '
             'terminated by the given string, and the line ending is returned to this plugin '
             'untranslated. '
             'Default: %(default)s',
        dest='NEWLINE',
        default=DEFAULT_NEWLINE,
    )

    parser.add_argument(
        '--no-proxy',
        help='Do not use a proxy. '
             'Default: %(default)s',
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--password',
        help='SMB or HTTP Basic Auth Password.',
        dest='PASSWORD',
    )

    parser.add_argument(
        '--quotechar',
        help='CSV quotechar. '
             'Default: `%(default)s`',
        dest='QUOTECHAR',
        default=DEFAULT_QUOTECHAR,
    )

    parser.add_argument(
        '--skip-header',
        help='Treat the first row as header names, and skip this row. '
             'Default: %(default)s',
        dest='SKIP_HEADER',
        action='store_true',
        default=DEFAULT_SKIP_HEADER,
    )

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

    parser.add_argument(
        '-u', '--url',
        help='Set the url of the CSV file, either starting with "http://", "https://" or '
             '"smb://". This is mutually exclusive with --filename.',
        dest='URL',
        type=str,
    )

    parser.add_argument(
        '--username',
        help='SMB or HTTP Basic Auth Username.',
        dest='USERNAME',
    )

    parser.add_argument(
        '-w', '--warning',
        help='Set the WARN threshold. Supports ranges.',
        dest='WARN',
    )

    parser.add_argument(
        '--warning-query',
        help='`SELECT` statement. If its result contains more than one column, the number of rows '
             'is checked against `--warning`, otherwise the single value is used.',
        dest='WARNING_QUERY',
        default='',
    )

    return parser.parse_args()


def get_state_and_value(conn, query, threshold, _type):
    """Execute SQL query, get the value and check against the threshold.
    * One row, one column: Check this single value.
    * x rows: Check the number of rows against the threshold.
    """
    state = STATE_OK
    value = 0
    result = []
    shortened = False
    if query:
        result = lib.base.coe(lib.db_sqlite.select(conn, query))
        if result:
            if len(result) == 1 and len(result[0]) == 1:
                # one row, one column: could be a "select count(*) from ..." result
                value = list(result[0].values())[0]
            else:
                # a bunch of rows (at least one) with multiple columns, so count them
                value = len(result)
                # shorten the result if there are too many rows
                if len(result) > 10:
                    # shorten the result
                    result = result[0:5] + result[-5:]
                    shortened = True

            if _type == 'warn':
                state = lib.base.get_state(value, threshold, None, _operator='range')
            else:
                state = lib.base.get_state(value, None, threshold, _operator='range')

    return state, value, result, shortened


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.WARNING_QUERY == '' and args.CRITICAL_QUERY == '':
        lib.base.cu('Nothing to check, no queries provided.')

    # fetch data in various ways (file, url, smb)
    if args.FILENAME and args.URL:
        lib.base.oao(
            'The --filename and -u / --url parameter are mutually exclusive. '
            'Please only use one.',
            STATE_UNKNOWN,
        )

    if args.FILENAME:
        db_filename = lib.db_sqlite.__sha1sum( # pylint: disable=W0212
            args.FILENAME + args.WARNING_QUERY + args.CRITICAL_QUERY
        )

    if args.URL:
        db_filename = lib.db_sqlite.__sha1sum( # pylint: disable=W0212
            args.URL + args.WARNING_QUERY + args.CRITICAL_QUERY
        )
        split_url = args.URL.split('://')
        if len(split_url) != 2:
            lib.base.oao(
                'Could not parse the protocol of the url "{}".'.format(args.URL),
                STATE_UNKNOWN,
            )
        proto, url = split_url
        if proto not in ['http', 'https', 'smb']:
            lib.base.cu('The protocol "{}" is not supported.'.format(proto))

        if proto in ['http', 'https']:
            header = {}
            if args.USERNAME and args.PASSWORD:
                auth = '{}:{}'.format(args.USERNAME, args.PASSWORD)
                encoded_auth = lib.txt.to_text(base64.b64encode(lib.txt.to_bytes(auth)))
                header['Authorization'] = 'Basic {}'.format(encoded_auth)
            csv = lib.base.coe(
                lib.url.fetch(
                    args.URL,
                    header=header,
                    insecure=args.INSECURE,
                    no_proxy=args.NO_PROXY,
                    timeout=args.TIMEOUT,
                )
            )
        else:
            if not HAVE_SMB:
                lib.base.oao(
                    'Python module "{}" is not installed.'.format(missing_lib), # pylint: disable=E0601
                    STATE_UNKNOWN,
                )
            with lib.base.coe(
                lib.smb.open_file(
                    url, args.USERNAME, args.PASSWORD, args.TIMEOUT
                )
            ) as fd:
                csv = lib.txt.to_text(fd.read())

        args.FILENAME = '{}/linuxfabrik-monitoring-plugins-csv-values-{}.csv'.format(
            lib.disk.get_tmpdir(),
            db_filename,
        )
        lib.base.coe(lib.disk.write_file(args.FILENAME, csv))

    # create the db file and import data
    conn = lib.base.coe(
        lib.db_sqlite.connect(
            filename='linuxfabrik-monitoring-plugins-csv-values-{}.db'.format(db_filename),
        )
    )
    lib.base.coe(
        lib.db_sqlite.import_csv(conn,
            args.FILENAME,
            fieldnames=args.COLUMNS_QUERY,
            skip_header=args.SKIP_HEADER,
            delimiter=args.DELIMITER,
            quotechar=args.QUOTECHAR,
            newline=args.NEWLINE,
            chunksize=args.CHUNKSIZE,
        )
    )

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

    # analyze data
    state_warn, cnt_warn, result_warn, shortened_warn = get_state_and_value(
        conn,
        args.WARNING_QUERY,
        args.WARN,
        'warn',
    )
    state = lib.base.get_worst(state, state_warn)
    state_crit, cnt_crit, result_crit, shortened_crit = get_state_and_value(
        conn,
        args.CRITICAL_QUERY,
        args.CRIT,
        'crit',
    )

    lib.db_sqlite.close(conn)
    state = lib.base.get_worst(state, state_crit)

    # build the message
    if args.WARNING_QUERY:
        msg = '{} {} from warning query `{}`{}'.format(
            cnt_warn,
            lib.txt.pluralize('result', cnt_warn),
            args.WARNING_QUERY,
            lib.base.state2str(state_warn, prefix=' '),
        )
    if args.WARNING_QUERY and args.CRITICAL_QUERY:
        msg += ' and '
    if args.CRITICAL_QUERY:
        msg += '{} {} from critical query `{}`{}'.format(
            cnt_crit,
            lib.txt.pluralize('result', cnt_crit),
            args.CRITICAL_QUERY,
            lib.base.state2str(state_crit, prefix=' '),
        )
    msg += '\n'

    if shortened_warn:
        msg += '\nAttention: Table below is truncated, showing the 5 first and the 5 last items.\n'
    try:
        keys = result_warn[0].keys()
        headers = keys
        msg += '\n' + lib.base.get_table(result_warn, keys, header=headers)
    except:
        # no results
        pass

    if shortened_crit:
        msg += '\nAttention: Table below is truncated, showing the 5 first and the 5 last items.\n'
    try:
        keys = result_crit[0].keys()
        headers = keys
        msg += '\n' + lib.base.get_table(result_crit, keys, header=headers)
    except:
        # no results
        pass

    perfdata += lib.base.get_perfdata('cnt_warn', cnt_warn, None, args.WARN, None, None, None)
    perfdata += lib.base.get_perfdata('cnt_crit', cnt_crit, None, None, args.CRIT, None, None)

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


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