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

import lib.args
import lib.base
import lib.db_sqlite
import lib.lftest
import lib.shell
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN

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

DESCRIPTION = """Checks for available APT package updates on Debian, Ubuntu, and compatible systems.
Reports the number of pending updates and upgrades, and alerts when updates are
available. This check only lists updates and never actually installs anything.
Requires root or sudo."""

DEFAULT_QUERY = '1'
DEFAULT_TIMEOUT = 60
DEFAULT_WARN = 1  # number of updatable packages


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(
        '--only-critical',
        help='Only report security-critical updates and upgrades.',
        dest='ONLY_CRITICAL',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--query',
        help='SQL WHERE clause to narrow down results from the internal updates table. '
        'Supports regular expressions via a REGEXP statement. '
        'If specified, a list of matching updates is printed. '
        'Have a look at the README for a list of available columns. '
        'Example: `--query=\'package like "bind9-%%"\'`. '
        'Default: %(default)s',
        dest='QUERY',
        default=DEFAULT_QUERY,
    )

    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(
        '-w',
        '--warning',
        help='Minimum number of pending packages to trigger a WARNING. '
        'Default: %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    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)

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

    # fetch data
    if args.TEST is None:
        # try to update the package cache first
        stdout, stderr, retc = lib.base.coe(
            lib.shell.shell_exec('sudo apt-get update --quiet 2', timeout=args.TIMEOUT),
        )
        if retc or stderr:
            # Not printing stderr as it is quite verbose
            msg += '`apt-get update` returned with an error. '
            lib.base.cu(msg)

        stdout, _, retc = lib.base.coe(
            lib.shell.shell_exec('apt list --upgradable', timeout=args.TIMEOUT),
        )
        if retc:
            # Not printing stderr as it is quite verbose
            msg += '`apt list --upgradable` returned with an error. '
            lib.base.cu(msg)
    else:
        # skip the apt-get update refresh in test mode and read the
        # `apt list --upgradable` output from a fixture file
        stdout, _, retc = lib.lftest.test(args.TEST)
        if retc:
            msg += '`apt list --upgradable` returned with an error. '
            lib.base.cu(msg)

    # strip first line "Listing... Done." (if any)
    first, _, rest = stdout.partition('\n')
    stdout = rest if first.lower().startswith('listing...') else stdout

    # create the db table (just one simple column, cause *apt does not have a stable CLI interface*)
    definition = """
        package TEXT DEFAULT NULL
    """
    conn = lib.base.coe(
        lib.db_sqlite.connect(
            filename='linuxfabrik-monitoring-plugins-deb-updates.db',
        )
    )
    lib.base.coe(
        lib.db_sqlite.create_table(
            conn,
            definition,
            table='deb_updates',
            drop_table_first=True,  # we don't need historical data
        )
    )

    # analyze data
    pattern = re.compile(r',\w*-security')
    for item in stdout.strip().splitlines():
        if args.ONLY_CRITICAL:
            if not bool(pattern.search(item)):  # if it is not critical
                continue
        lib.base.coe(lib.db_sqlite.insert(conn, {'package': item}, table='deb_updates'))

    # store table_data in local sqlite database
    lib.base.coe(lib.db_sqlite.commit(conn))

    # fetch desired objects only, and set sqlite3 to be case insensitive when string comparing
    # QUERY is by design an admin-provided SQL WHERE clause (documented feature)
    sql = f"""
        SELECT *
        FROM deb_updates
        WHERE {args.QUERY}
        COLLATE NOCASE
    """  # nosec B608
    result = lib.base.coe(lib.db_sqlite.select(conn, sql))
    lib.db_sqlite.close(conn)

    # get state
    state = lib.base.get_state(len(result), args.WARN, None)

    # build the message
    if len(result) == 0:
        msg += f'No updates available{" (query: " + args.QUERY + ")" if args.QUERY != "1" else ""}.'
    else:
        msg += f'{len(result)} {"critical " if args.ONLY_CRITICAL else ""}'
        msg += f'{lib.txt.pluralize("update", len(result))} available'
        msg += f'{" (query: " + args.QUERY + ")" if args.QUERY != "1" else ""}'
        msg += '.'
        msg += lib.base.state2str(state, prefix=' ')
        msg += '\n'
        msg += '\n* '.join([row['package'] for row in result])
    perfdata += lib.base.get_perfdata(
        'updates',
        len(result),
        warn=args.WARN,
        _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()
