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

import lib.args  # pylint: disable=C0413
import lib.base  # pylint: disable=C0413
import lib.shell  # pylint: disable=C0413
import lib.lftest  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
from lib.globals import (STATE_OK, STATE_UNKNOWN)

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

DESCRIPTION = """Checks if there are outdated Python packages, installed via `pip`."""

DEFAULT_WARN = 10
DEFAULT_CRIT = 100


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='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c', '--critical',
        help='Set the critical threshold for the number of pending updates. '
             'Default: %(default)s',
        dest='CRIT',
        default=DEFAULT_CRIT,
        type=lib.args.int_or_none,
    )

    parser.add_argument(
        '--exclude',
        help='Exclude specified package from the output.',
        dest='EXCLUDE',
        action='append',
    )

    parser.add_argument(
        '--extra-index-url',
        help='Extra URLs of package indexes to use in addition to '
             '--index-url. Should follow the same rules as '
             '--index-url.',
        dest='EXTRA_INDEX_URL',
        action='append',
        default=[],
    )

    parser.add_argument(
        '--find-links',
        help='If a URL or path to an html file, then parse for links to '
             'archives such as sdist (.tar.gz) or wheel (.whl) files. '
             'If a local path or file:// URL that\'s a directory, '
             'then look for archives in the directory listing. '
             'Links to VCS project URLs are not supported.',
        dest='FIND_LINKS',
        action='append',
        default=[],
    )

    parser.add_argument(
        '--index-url',
        help='Base URL of the Python Package Index. '
             'This should point to a repository compliant with PEP 503 '
             '(the simple repository API) or a local directory laid out '
             'in the same format.',
        dest='INDEX_URL',
    )

    parser.add_argument(
        '--local',
        help='If in a virtualenv that has global access, do not list '
             'globally-installed packages.',
        dest='LOCAL',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--no-index',
        help='Ignore package index (only looking at --find-links URLs instead).',
        dest='NO_INDEX',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--not-required',
        help='List packages that are not dependencies of installed packages.',
        dest='NOT_REQUIRED',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--pre',
        help='Include pre-release and development versions. By default, '
             'pip only finds stable versions.',
        dest='PRE',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--test',
        help='For unit tests. Needs "path-to-stdout-file,path-to-stderr-file,expected-retc".',
        dest='TEST',
        type=lib.args.csv,
    )

    parser.add_argument(
        '--user',
        help='Only output packages installed in user-site.',
        dest='USER',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--virtualenv',
        help='Path to the virtualenv that will be activated before checking for updates. '
             'Example: `/opt/sphinx-venv/bin/activate`',
        dest='VIRTUALENV',
    )

    parser.add_argument(
        '-w', '--warning',
        help='Set the warning threshold for the number of pending updates. '
             'Default: %(default)s',
        dest='WARN',
        default=DEFAULT_WARN,
        type=lib.args.int_or_none,
    )

    return parser.parse_args()


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)

    # fetch data
    cmd = 'python3 -m pip list --outdated --format=json'
    if args.TEST is None:
        # build commandline call
        if args.EXCLUDE:
            cmd += f' --exclude={" --exclude=".join(args.EXCLUDE)}'
        if args.EXTRA_INDEX_URL:
            cmd += f' --extra-index-url={" --extra-index-url=".join(args.EXTRA_INDEX_URL)}'
        if args.FIND_LINKS:
            cmd += f' --find-links={" --find-links=".join(args.FIND_LINKS)}'
        if args.INDEX_URL:
            cmd += f' --index-url={args.INDEX_URL}'
        if args.LOCAL:
            cmd += ' --local'
        if args.NO_INDEX:
            cmd += ' --no-index'
        if args.NOT_REQUIRED:
            cmd += ' --not-required'
        if args.PRE:
            cmd += ' --pre'
        if args.USER:
            cmd += ' --user'

        if args.VIRTUALENV:
            # `source` does not work on every environment, so use `.`
            cmd = f'. {args.VIRTUALENV} && {cmd}'
            stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(cmd, shell=True))
        else:
            stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(cmd))
    else:
        # do not call the command, put in test data
        stdout, stderr, retc = lib.lftest.test(args.TEST)
    if retc != 0:
        lib.base.cu(stderr)

    # analyze data
    try:
        packages = json.loads(stdout)
    except json.decoder.JSONDecodeError:
        lib.base.cu('Failed to parse JSON.')

    # init some vars
    if args.VIRTUALENV:
        msg = f'venv {args.VIRTUALENV}. '
    else:
        msg = 'Not running in a venv. '
    state = lib.base.get_state(len(packages), args.WARN, args.CRIT)
    perfdata = lib.base.get_perfdata(
        'pip_outdated_packages',
        len(packages),
        uom=None,
        warn=args.WARN,
        crit=args.CRIT,
        _min=0,
        _max=None,
    )

    # build the message
    if not stderr and state == STATE_OK:
        msg += 'Everything is ok. '
    elif stderr and state == STATE_OK:
        msg += 'pip is complaining about something or about itself, but '
        if len(packages) > 0:
            msg += 'most of the '
        msg += 'packages are up to date. '
    elif stderr and state != STATE_OK:
        msg += 'pip is complaining about something or about itself, plus '
    if len(packages) > 0:
        msg += f'{len(packages)} outdated {lib.txt.pluralize("package", len(packages))}' \
               f'{lib.base.state2str(state, prefix=" ")}. '
    msg += f'Executed command: `{cmd.replace(" --format=json", "")}`'

    if packages:
        msg += '\n\n'
        keys = [
            'name',
            'version',
            'latest_version',
            'latest_filetype',
        ]
        headers = [
            'Package',
            'Version',
            'Latest',
            'Type',
        ]
        msg += lib.base.get_table(packages, keys, header=headers)

    # 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()
