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

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

DESCRIPTION = """Checks the age of the newest snapshot in a restic repository. Alerts when the most
recent backup is older than the configured thresholds. Useful for detecting failed or
missing backup runs.
Supports extended reporting via --lengthy. Requires root or sudo."""

DEFAULT_CRIT = None
DEFAULT_GROUP_BY = 'host,paths'
DEFAULT_LATEST = 3  # count
DEFAULT_LENGTHY = False
DEFAULT_WARN = 24  # hours


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(
        '-c',
        '--critical',
        help='CRIT threshold for the age of the newest snapshot in each group, in hours. '
        'Default: %(default)s',
        dest='CRIT',
        type=int,
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--group-by',
        help='Comma-separated list of fields to group snapshots by. '
        'Allowed values: host, paths, tags. '
        'Default: %(default)s',
        dest='GROUP_BY',
        default=DEFAULT_GROUP_BY,
    )

    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(
        '--latest',
        help='Number of latest snapshots to show per host and path. '
        'Default: %(default)s',
        dest='LATEST',
        type=int,
        default=DEFAULT_LATEST,
    )

    parser.add_argument(
        '--lengthy',
        help=lib.args.help('--lengthy'),
        dest='LENGTHY',
        action='store_true',
        default=DEFAULT_LENGTHY,
    )

    parser.add_argument(
        '--password-file',
        help='Path to the file containing the repository password.',
        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.',
        dest='REPO',
        required=True,
    )

    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(
        '-w',
        '--warning',
        help='WARN threshold for the age of the newest snapshot in each group, in hours. '
        'Default: %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


def get_snapshot_data(snapshot, args):
    """Enrich data returned from retic's snapshot command."""
    snapshot['ts'] = snapshot['time'][0:19].replace(
        'T', ' '
    )  # 2022-11-28T16:22:43.896818692Z
    snapshot['age'] = lib.time.timestrdiff(lib.time.now('iso'), snapshot['ts'])
    snapshot['age_hr'] = lib.human.seconds2human(snapshot['age'])  # human-readable
    snapshot['p'] = ' '.join(snapshot['paths'])
    snapshot['tags'] = ','.join(snapshot['tags']) if 'tags' in snapshot else ''
    snapshot['state'] = lib.base.get_state(
        int(snapshot['age'] / 3600), args.WARN, args.CRIT
    )

    return snapshot


def get_message(table_data, args):
    """Build the message. Show latest/newest snapshot on top, which is the last one in the list."""
    latest = table_data[-1]
    snap_count = len(table_data)
    msg = (
        f'\n\nLatest snapshot {latest["age_hr"]} ago'
        f'{lib.base.state2str(latest["state"], prefix=" ")}'
        f' ({latest["ts"]}@{latest["hostname"]}'
        f':{latest["p"]}, ID {latest["short_id"]});'
        f' {snap_count}'
        f' {lib.txt.pluralize("snapshot", snap_count)}'
        f' found\n\n'
    )
    table_data[-1]['age_hr'] += lib.base.state2str(table_data[-1]['state'], prefix=' ')
    if table_data:
        if not args.LENGTHY:
            keys = [
                'short_id',
                'ts',
                'age_hr',
                'hostname',
                'p',
                #'username',
            ]
            headers = [
                'Short ID',
                'Timestamp',
                'Age',
                'Host',
                'Paths',
                #'User',
            ]
        else:
            keys = [
                'short_id',
                'ts',
                'age_hr',
                'hostname',
                'p',
                'tags',
                #'username',
            ]
            headers = [
                'Short ID',
                'Timestamp',
                'Age',
                'Host',
                'Paths',
                'Tags',
                #'User',
            ]
        msg += lib.base.get_table(
            table_data,
            keys,
            header=headers,
            sort_by_key='ts',
            sort_order_reverse=True,
        )

    return msg, table_data[-1]['state']


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 = (
            f'restic --json --repo={args.REPO}'
            f' --password-file={args.PASSWORD_FILE}'
            f' --latest={args.LATEST}'
        )
        if args.HOST:
            cmd += ' --host=' + ' --host='.join(args.HOST)
        if args.PATH:
            cmd += ' --path=' + ' --path='.join(args.PATH)
        if args.TAG:
            cmd += ' --tag=' + ' --tag='.join(args.TAG)
        if args.GROUP_BY:
            cmd += f' --group-by={args.GROUP_BY}'
        cmd += ' snapshots'
        stdout, stderr, _retc = lib.base.coe(lib.shell.shell_exec(cmd))
        if stderr:
            lib.base.cu(stderr)
    else:
        # do not call the command, put in test data
        stdout, stderr, _retc = lib.lftest.test(args.TEST)

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

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    table_data = []

    if not snapshots:
        lib.base.oao(
            f'No snapshots match your filter criteria. Called `{cmd}`',
            STATE_UNKNOWN,
        )

    # analyze data and build the message
    snapshot_count = 0
    if 'group_key' in snapshots[0]:
        # --group-by was used, so check every group of snapshots
        for snapshot in snapshots:
            table_data = []
            for s in snapshot['snapshots']:
                table_data.append(get_snapshot_data(s, args))
            snapshot_msg, snapshot_state = get_message(table_data, args)
            snapshot_count += len(table_data)

            # build the message
            msg += snapshot_msg
            state = lib.base.get_worst(snapshot_state, state)
    else:
        for snapshot in snapshots:
            table_data.append(get_snapshot_data(snapshot, args))
        msg, state = get_message(table_data, args)
        snapshot_count = len(table_data)

    perfdata += lib.base.get_perfdata(
        'snapshots',
        snapshot_count,
        uom='',
        _min=0,
    )

    if state == STATE_CRIT:
        msg = 'There are critical errors.' + msg
    elif state == STATE_WARN:
        msg = 'There are warnings.' + msg
    else:
        msg = 'Everything is ok.' + msg

    # over and out
    lib.base.oao(msg, state, perfdata)


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