#!/usr/lib64/linuxfabrik-monitoring-plugins/venv/bin/python
# -*- 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.human
import lib.lftest
import lib.restic
import lib.shell
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Collects statistics across multiple snapshots in a restic repository, including the
number of unique files and their total size. Supports different counting modes
(restore-size, files-by-contents, blobs-per-file, raw-data). The figures are reported for
trending and never alert on their own; alerts only if restic fails or times out.
Requires root or sudo."""

DEFAULT_MODE = 'restore-size'
DEFAULT_TIMEOUT = 280  # the command timeout of the basket is 300


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(
        '--host',
        help='Only consider snapshots for this host. Can be specified multiple times.',
        dest='HOST',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--mode',
        help='Counting mode for the statistics calculation. Default: %(default)s',
        dest='MODE',
        choices=['restore-size', 'files-by-contents', 'blobs-per-file', 'raw-data'],
        default=DEFAULT_MODE,
    )

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

    parser.add_argument(
        '--password-file',
        help='Path to the file containing the repository password. '
        'Must be a file that only root can change.',
        dest='PASSWORD_FILE',
    )

    parser.add_argument(
        '--path',
        help='Only consider snapshots for this path. Can be specified multiple times.',
        dest='PATH',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--repo',
        help='Restic repository location. '
        'A local repository has to be a directory that only root can change. '
        'Give exactly one of --repo and --repository-file.',
        dest='REPO',
        default=None,
    )

    parser.add_argument(
        '--repository-file',
        help='Path to a file whose first line is the repository location. '
        'Use this instead of --repo for a repository whose location selects an '
        'external helper (an `rclone:` repository): the file must be one that only '
        'root can change, so an unprivileged caller cannot choose what runs. '
        'Give exactly one of --repo and --repository-file.',
        dest='REPOSITORY_FILE',
    )

    parser.add_argument(
        '--tag',
        help='Only consider snapshots matching this taglist in the format `tag[,tag,...]`. '
        'Can be specified multiple times.',
        dest='TAG',
        action='append',
        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,
    )

    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)

    # fetch data
    if args.TEST is None:
        cmd = [
            'restic',
            '--json',
            *lib.restic.READ_ONLY_ARGS,
            *lib.base.coe(lib.restic.repo_args(args.REPO, args.REPOSITORY_FILE)),
            *lib.base.coe(lib.restic.password_file_arg(args.PASSWORD_FILE)),
            f'--mode={args.MODE}',
        ]
        if args.HOST:
            cmd += [f'--host={host}' for host in args.HOST]
        if args.PATH:
            cmd += [f'--path={path}' for path in args.PATH]
        if args.TAG:
            cmd += [f'--tag={tag}' for tag in args.TAG]
        cmd.append('stats')
        success, result = lib.shell.shell_exec(cmd, timeout=args.TIMEOUT)
        if not success and result.startswith('Timeout after'):
            lib.base.oao(
                f'Timeout after {args.TIMEOUT}s while running `restic stats`.',
                STATE_WARN,
                always_ok=args.ALWAYS_OK,
            )
        stdout, stderr, retc = lib.base.coe((success, result))
        if retc != 0:
            lib.base.oao(
                f'`restic stats` failed (exit {retc}): '
                + ' '.join(lib.restic.error_lines(stderr)),
                STATE_WARN,
                always_ok=args.ALWAYS_OK,
            )
    else:
        # do not call the command, put in test data
        stdout, stderr, _retc = lib.lftest.test(args.TEST)

    try:
        stats = json.loads(stdout)
    except ValueError:
        lib.base.cu('No JSON object could be decoded.')

    # init some vars
    state = STATE_OK
    perfdata = ''

    # analyze data
    total_size = stats.get('total_size', 0)
    # restic only counts files in the restore-size and files-by-contents modes; in
    # raw-data and blobs-per-file the count is absent (`total_file_count` carries
    # `omitempty` since restic v0.16 and is left out when it is 0), so it is
    # reported only where restic supplies it.
    total_file_count = stats.get('total_file_count')

    # build the message
    if total_file_count is not None:
        msg = (
            f'{lib.human.number2human(total_file_count)}'
            f' files,'
            f' {lib.human.bytes2human(total_size)} size'
            f' (total stats in {args.MODE} mode'
            f' over all snapshots)'
        )
    else:
        msg = (
            f'{lib.human.bytes2human(total_size)} size'
            f' (total stats in {args.MODE} mode'
            f' over all snapshots)'
        )
    perfdata += lib.base.get_perfdata(
        'total_size',
        total_size,
        uom='',
        _min=0,
    )
    if total_file_count is not None:
        perfdata += lib.base.get_perfdata(
            'total_file_count',
            total_file_count,
            uom='',
            _min=0,
        )

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