#!/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 zipfile

import lib.args
import lib.base
import lib.lftest
import lib.txt
import lib.version
from lib.globals import STATE_UNKNOWN

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

DESCRIPTION = """Checks the installed Metabase version against the endoflife.date API and alerts if the
version is end-of-life or if newer major, minor, or patch releases are available. By
default, alerts 30 days before the official EOL date. The offset is configurable."""

DEFAULT_CHECK_MAJOR = False
DEFAULT_CHECK_MINOR = False
DEFAULT_CHECK_PATCH = False
DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_OFFSET_EOL = -30  # days
DEFAULT_PATH = '/opt/metabase/metabase.jar'
DEFAULT_TIMEOUT = 8
DEFAULT_UNREACHABLE_SEVERITY = 'ok'

# The jar entry holding the version is a handful of `key=value` lines. Reading it
# without looking at its announced size first would hand a crafted archive the chance
# to expand a few kilobytes into gigabytes of memory.
MAX_PROPERTIES_BYTES = 64 * 1024
VERSION_PROPERTIES = 'version.properties'


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    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(
        '--check-major',
        help=lib.args.help('--check-major'),
        dest='CHECK_MAJOR',
        action='store_true',
        default=DEFAULT_CHECK_MAJOR,
    )

    parser.add_argument(
        '--check-minor',
        help=lib.args.help('--check-minor'),
        dest='CHECK_MINOR',
        action='store_true',
        default=DEFAULT_CHECK_MINOR,
    )

    parser.add_argument(
        '--check-patch',
        help=lib.args.help('--check-patch'),
        dest='CHECK_PATCH',
        action='store_true',
        default=DEFAULT_CHECK_PATCH,
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--offset-eol',
        help=lib.args.help('--offset-eol') + ' Default: %(default)s days',
        dest='OFFSET_EOL',
        type=int,
        default=DEFAULT_OFFSET_EOL,
    )

    parser.add_argument(
        '--path',
        help="Full path to Metabase's `metabase.jar`. Default: %(default)s",
        dest='PATH',
        default=DEFAULT_PATH,
    )

    parser.add_argument(
        '--proxy',
        help=lib.args.help('--proxy'),
        dest='PROXY',
        default=None,
    )

    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(
        '--unreachable-severity',
        help=lib.args.help('--unreachable-severity') + ' Default: %(default)s',
        dest='UNREACHABLE_SEVERITY',
        choices=['ok', 'warn', 'crit', 'unknown'],
        default=DEFAULT_UNREACHABLE_SEVERITY,
    )

    args, _ = parser.parse_known_args()
    return args


def read_version_properties(path):
    """Read `version.properties` out of the Metabase jar.

    Metabase's build writes the file into the root of the jar, and the `version` command
    of that same jar only prints its contents back after a full Metabase JVM has come
    up. That start takes about 30 seconds on a production host, registers the database
    drivers and opens the application database environment, which is far more than a
    check may spend and far more than it needs to touch. Upstream reads the file
    directly for the same reason (`bin/build-backend-for-test`: "pulling the
    version.properties directly from the jar is much faster than starting the JVM").

    Parameters
    ----------
    path : str
        Path to the Metabase jar.

    Returns
    -------
    tuple (bool, str)
        The contents of the file on success, an admin-facing error message otherwise.
    """
    try:
        with zipfile.ZipFile(path) as jar:
            info = jar.getinfo(VERSION_PROPERTIES)
            if info.file_size > MAX_PROPERTIES_BYTES:
                return False, (
                    f'`{path}` does not look like a Metabase jar: its '
                    f'`{VERSION_PROPERTIES}` is {info.file_size} bytes. '
                    'Use --path to point at the Metabase jar.'
                )
            raw = jar.read(info)
    except FileNotFoundError:
        return False, (
            f'Metabase not found at `{path}`. Use --path to point at the Metabase jar.'
        )
    except PermissionError:
        return False, (
            f'No permission to read `{path}`. Grant the monitoring user read access to '
            'the Metabase jar, or use --path to point at a readable copy.'
        )
    except KeyError:
        return False, (
            f'`{path}` carries no `{VERSION_PROPERTIES}`, so it is not a Metabase jar. '
            'Use --path to point at the Metabase jar.'
        )
    except (OSError, zipfile.BadZipFile) as e:
        return False, f'Unable to read `{path}`: {e}'

    return True, lib.txt.to_text(raw, errors='strict_or_latin1')


def get_installed_version(path, test_arg=None):
    """Pick the release Metabase names out of its `version.properties`.

    Verified against the released jars metabase.jar v0.58.24 and v1.58.24 (2026-09-07),
    both carrying `tag=v...`, `hash=03d994f`, `date=2026-08-05`; the v0.47.13 jar adds a
    `branch=?` line, so the file is read by key and never by line position.

    Parameters
    ----------
    path : str
        Path to the Metabase jar.
    test_arg : list, optional
        `--test` fixture standing in for the contents of
        `version.properties`.

    Returns
    -------
    tuple (bool, str)
        The version on success, an admin-facing error message otherwise.
    """
    if test_arg is None:
        success, properties = read_version_properties(path)
        if not success:
            return False, properties
    else:
        properties, _, _ = lib.lftest.test(test_arg)

    # `tag=v0.58.24`, `tag=v1.58.24` for the Enterprise Edition, and shapes like
    # `tag=v0.59.0-SNAPSHOT` (a build off a branch) or `tag=v1.56.2-X01` (a Metabase
    # Cloud build), whose suffixes carry no release information for us.
    match = re.search(r'^tag=v?(\d+(?:\.\d+)*)', properties, re.MULTILINE)
    if not match:
        return False, (
            f'`{path}` names no release in its `{VERSION_PROPERTIES}`. This is what a '
            'Metabase built from a source checkout looks like; a released jar always '
            'carries its version. Use --path to point at the released Metabase jar.'
        )
    return True, match.group(1)


def to_release_version(installed_version):
    """Drop Metabase's license digit so the version can be looked up.

    Metabase prefixes every release with the digit of its license, `0` for the
    open-source edition and `1` for the Enterprise Edition, and both editions ship the
    same release behind it: v0.58.24 and v1.58.24 carry the identical `hash=03d994f`
    and `date=2026-08-05`. endoflife.date catalogues the open-source cycles only, so an
    Enterprise tag looked up as it stands would sit above every cycle listed there and
    report "newer than anything endoflife.date lists" on every run, for as long as the
    host keeps running it.

    Parameters
    ----------
    installed_version : str
        Version as the jar names it, for example `1.58.24`.

    Returns
    -------
    str
        The same version with the license digit normalized to `0`.

    Examples
    --------
    >>> to_release_version('1.58.24')
    '0.58.24'
    >>> to_release_version('0.58.24')
    '0.58.24'
    """
    return re.sub(r'^\d+', '0', installed_version, count=1)


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)

    # a `--test` without a value carries no fixture at all, rather than one named the
    # empty string
    if args.TEST is not None and not any(args.TEST):
        args.TEST = None

    # fetch data
    installed_version = lib.base.coe(get_installed_version(args.PATH, args.TEST))

    # init some vars
    perfdata = lib.base.get_perfdata(
        'metabase-version',
        lib.version.version2float(installed_version),
        uom=None,
        _min=0,
    )

    # analyze data
    state, eol = lib.version.check_eol(
        'https://endoflife.date/api/metabase.json',
        to_release_version(installed_version),
        offset_eol=args.OFFSET_EOL,
        check_major=args.CHECK_MAJOR,
        check_minor=args.CHECK_MINOR,
        check_patch=args.CHECK_PATCH,
        insecure=args.INSECURE,
        no_proxy=args.NO_PROXY,
        proxy=args.PROXY,
        timeout=args.TIMEOUT,
        unreachable_severity=args.UNREACHABLE_SEVERITY,
    )

    # build the message
    msg = f'Metabase v{installed_version} ({eol})'

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


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