#!/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 os
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__ = '2026061201'

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. The interpreter from that '
        'virtualenv is used to check for updates inside it. '
        '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
        for item in args.EXCLUDE or []:
            cmd.append(f'--exclude={item}')
        for item in args.EXTRA_INDEX_URL or []:
            cmd.append(f'--extra-index-url={item}')
        for item in args.FIND_LINKS or []:
            cmd.append(f'--find-links={item}')
        if args.INDEX_URL:
            cmd.append(f'--index-url={args.INDEX_URL}')
        if args.LOCAL:
            cmd.append('--local')
        if args.NO_INDEX:
            cmd.append('--no-index')
        if args.NOT_REQUIRED:
            cmd.append('--not-required')
        if args.PRE:
            cmd.append('--pre')
        if args.USER:
            cmd.append('--user')

        if args.VIRTUALENV:
            # Run the virtualenv's own interpreter instead of sourcing its activate
            # script, which avoids a shell. VIRTUALENV points at the activate script,
            # so its directory holds the interpreter.
            cmd[0] = os.path.join(os.path.dirname(args.VIRTUALENV), 'python3')

        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=" ")}. '
        )
    cmd_str = ' '.join(token for token in cmd if token != '--format=json')
    msg += f'Executed command: `{cmd_str}`'

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