#!/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.lftest
import lib.restic
import lib.shell
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Verifies the integrity of a restic backup repository by running "restic check". Alerts
when the repository contains errors or inconsistencies.
Requires root or sudo."""

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(
        '--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(
        '--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(
        '--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 get_check_summary(stdout):
    """Return the summary `restic check --json` prints, or None for plain text.

    Since restic v0.18 `check --json` writes a single `summary` object to stdout,
    carrying `num_errors` and the repair hints. Older versions ignore `--json` for
    `check` and print human-readable text, which is recognised here by not parsing
    as JSON.
    """
    for line in stdout.splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            message = json.loads(line)
        except ValueError:
            return None
        if isinstance(message, dict) and message.get('message_type') == 'summary':
            return message
    return None


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)),
            'check',
        ]
        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 check`.',
                STATE_WARN,
                always_ok=args.ALWAYS_OK,
            )
        stdout, stderr, retc = lib.base.coe((success, result))
    else:
        # do not call the command, put in test data
        stdout, stderr, retc = lib.lftest.test(args.TEST)
    # 1 is what `check` returns for a damaged repository, anything else means it could
    # not check at all, such as 10 (no repository there) or 12 (wrong password)
    if retc not in (0, 1):
        lib.base.oao(
            f'`restic check` failed (exit {retc}): '
            + ' '.join(lib.restic.error_lines(stderr)),
            STATE_WARN,
            always_ok=args.ALWAYS_OK,
        )

    # init some vars
    state = STATE_OK
    details = []
    hints = []

    # analyze data
    summary = get_check_summary(stdout)
    if summary is not None:
        # restic >= v0.18: the JSON summary decides the state
        if retc != 0 or summary.get('num_errors', 0) != 0:
            state = STATE_WARN
            if summary.get('suggest_repair_index'):
                hints.append('restic repair index')
            if summary.get('suggest_prune'):
                hints.append('restic prune')
            details = lib.restic.error_lines(stderr)
    # restic < v0.18: `check` ignores --json and prints human-readable text
    elif retc != 0 or 'no errors' not in stdout:
        state = STATE_WARN
        details = lib.restic.error_lines(stderr) or lib.restic.error_lines(stdout)

    # build the message
    if state == STATE_OK:
        msg = 'Everything is ok.'
    else:
        msg = 'There are errors.'
        if details:
            msg += '\n\n' + '\n'.join(details)
        if hints:
            msg += '\n\nSuggested: ' + ', '.join(f'`{hint}`' for hint in hints)

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


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