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

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

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

DESCRIPTION = """Checks for outdated Python packages installed via pip. Reports the number of packages
with available updates and lists them. Alerts when the count exceeds the configured
threshold."""

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=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

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

    parser.add_argument(
        '--exclude',
        help='Package name to exclude from the output. '
        'Can be specified multiple times.',
        dest='EXCLUDE',
        action='append',
    )

    parser.add_argument(
        '--extra-index-url',
        help='Extra URL of a package index to use in addition to --index-url. '
        'Should follow the same rules as --index-url. '
        'Can be specified multiple times.',
        dest='EXTRA_INDEX_URL',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--find-links',
        help='URL or path to an HTML file to parse for links to archives (.tar.gz, .whl). '
        'If a local path or file:// URL pointing to a directory, look for archives in the directory listing. '
        'VCS project URLs are not supported. '
        'Can be specified multiple times.',
        dest='FIND_LINKS',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--index-url',
        help='Base URL of the Python Package Index (PEP 503 compliant repository or local directory 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 the package index and only look at --find-links URLs.',
        dest='NO_INDEX',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--not-required',
        help='Only list packages that are not dependencies of other 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=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

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

    parser.add_argument(
        '--virtualenv',
        help='Path to a virtualenv activate script to source before checking for updates. '
        'Example: `/opt/sphinx-venv/bin/activate`',
        dest='VIRTUALENV',
    )

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

    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)

    # set default values for append parameters that were not specified
    if args.EXTRA_INDEX_URL is None:
        args.EXTRA_INDEX_URL = []
    if args.FIND_LINKS is None:
        args.FIND_LINKS = []

    # 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 `.`
            # shell=True needed for the `. venv && pip` composition; VIRTUALENV
            # is an admin-provided path from the Icinga check config
            cmd = f'. {args.VIRTUALENV} && {cmd}'
            stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(cmd, shell=True))  # nosec B604
        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
    msg = f'venv {args.VIRTUALENV}. ' if args.VIRTUALENV else '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:
        lib.base.cu()
