#!/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  # pylint: disable=C0413
import sys  # pylint: disable=C0413
from pathlib import Path  # pylint: disable=C0413

import lib.base  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413

try:
    import lib.smb  # pylint: disable=C0413
    HAVE_SMB = True
except ModuleNotFoundError as e:
    HAVE_SMB = False
    missing_lib = e.name
import lib.time  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

missing_smb_lib = False
try:
    import smbprotocol.exceptions
except ImportError as e:
    missing_smb_lib = 'smbclient'


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

DESCRIPTION = 'Checks the time of last data modification for a file or directory, in seconds.'


DEFAULT_CRIT = 60*60*24*365     # sec
DEFAULT_WARN = 60*60*24*30      # sec
DEFAULT_CRIT_COUNT = 0
DEFAULT_WARN_COUNT = 0
DEFAULT_PATTERN = '*'
DEFAULT_TIMEOUT = 3
DEFAULT_PERFDATA_MODE = None


def parse_args():
    """Parse command line arguments using argparse.
    """
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V', '--version',
        action='version',
        version='%(prog)s: v{} by {}'.format(__version__, __author__)
    )

    parser.add_argument(
        '--always-ok',
        help='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c', '--critical',
        help='Set the critical age threshold in seconds. '
             'Supports ranges. '
             'Default: >= %(default)ss (365d)',
        dest='CRIT',
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--critical-count',
        help='Set the critical threshold for the number of files found within the critical age. '
             'Supports ranges. '
             'Default: > %(default)s',
        dest='CRIT_COUNT',
        default=DEFAULT_CRIT_COUNT,
    )

    parser.add_argument(
        '--filename',
        help='File or directory name to check. '
             'Supports glob in accordance with https://docs.python.org/2.7/library/glob.html. '
             'Beware of using recursive globs. This is mutually exclusive with -u / --url.',
        dest='FILENAME',
    )

    parser.add_argument(
        '--only-dirs',
        help='Only consider directories.',
        dest='ONLY_DIRS',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--only-files',
        help='Only consider files.',
        dest='ONLY_FILES',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--password',
        help='SMB: Password.',
        dest='PASSWORD',
    )

    parser.add_argument(
        '--pattern',
        help="SMB: The search string to match against the names of SMB directories or files. "
             "This pattern can use '*' as a wildcard for multiple chars and '?' as a wildcard for "
             "a single char. Does not support regex patterns. "
             "Default: %(default)s.",
        dest='PATTERN',
        default=DEFAULT_PATTERN,
    )

    parser.add_argument(
        '--perfdata-mode',
        help='Set the performance data aggregation mode. '
             'Default: %(default)s.',
        dest='PERFDATA_MODE',
        default=DEFAULT_PERFDATA_MODE,
        choices=[
            'mean',
            'median',
            'None',
        ],
    )

    parser.add_argument(
        '--timeout',
        help='Network timeout in seconds. '
             'Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    parser.add_argument(
        '-u', '--url',
        help='SMB: Set the url of the file (or directory) to check, starting with "smb://". '
             'This is mutually exclusive with --filename.',
        dest='URL',
        type=str,
    )

    parser.add_argument(
        '--username',
        help='SMB: Username.',
        dest='USERNAME',
    )

    parser.add_argument(
        '-w', '--warning',
        help='Set the warning age threshold in seconds. '
             'Supports ranges. '
             'Default: >= %(default)ss (30d)',
        dest='WARN',
        default=DEFAULT_WARN,
    )

    parser.add_argument(
        '--warning-count',
        help='Set the warning threshold for the number of files found within the warning age. '
             'Supports ranges. '
             'Default: > %(default)s (30d)',
        dest='WARN_COUNT',
        default=DEFAULT_WARN_COUNT,
    )

    return parser.parse_args()


def main():
    """The main function. Hier spielt die Musik.
    """

    # parse the command line, exit with UNKNOWN if it fails
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    if args.FILENAME and args.URL:
        lib.base.cu('The --filename and -u / --url parameter are mutually exclusive. Please only use one.')
    if args.URL:
        split_url = args.URL.split('://')
        if len(split_url) != 2:
            lib.base.cu('Could not parse the protocol of the url "{}".'.format(args.URL))
        proto, url = split_url
        if proto != 'smb':
            lib.base.cu('The protocol "{}" is not supported.'.format(proto))

    file_msg = ''
    file_count = 0
    crit_count = 0
    warn_count = 0
    ages = []

    if args.FILENAME:
        path = Path(args.FILENAME)
        for item in sorted(Path(path.anchor).glob(str(path.relative_to(path.anchor)))):
            if item.is_file() and args.ONLY_DIRS:
                continue
            if item.is_dir() and args.ONLY_FILES:
                continue

            # brandnew files might get negative values
            age = abs(lib.time.now() - item.stat().st_mtime)
            ages.append(age)
            item_state = STATE_OK
            # not using elif, as the item could contribute to both the warn_count and crit_count
            if not lib.base.coe(lib.base.match_range(age, args.WARN)):
                item_state = STATE_WARN
                warn_count += 1
            if not lib.base.coe(lib.base.match_range(age, args.CRIT)):
                item_state = STATE_CRIT
                crit_count += 1

            file_count += 1
            if item_state != STATE_OK:
                file_msg += '* {}: {}{}\n'.format(
                    item,
                    lib.human.seconds2human(age),
                    lib.base.state2str(item_state, prefix=" "),
                )

    if args.URL:
        if not HAVE_SMB:
            lib.base.cu('Python module "{}" is not installed.'.format(missing_lib))
        for item in lib.base.coe(lib.smb.glob(url, args.USERNAME, args.PASSWORD, args.TIMEOUT, pattern=args.PATTERN)):
            try:
                if item.is_file() and args.ONLY_DIRS:
                    continue
                if item.is_dir() and args.ONLY_FILES:
                    continue

                # brandnew files might get negative values
                age = abs(lib.time.now() - item.stat().st_mtime)
                ages.append(age)
                item_state = STATE_OK
                # not using elif, as the item could contribute to both the warn_count and crit_count
                if not lib.base.coe(lib.base.match_range(age, args.WARN)):
                    item_state = STATE_WARN
                    warn_count += 1
                if not lib.base.coe(lib.base.match_range(age, args.CRIT)):
                    item_state = STATE_CRIT
                    crit_count += 1

                file_count += 1
                if item_state != STATE_OK:
                    file_msg += '* {}: {}{}\n'.format(
                        item.name,
                        lib.human.seconds2human(age),
                        lib.base.state2str(item_state, prefix=" "),
                    )
            except smbprotocol.exceptions.SMBOSError:
                # it is normal that files disappear while reading
                pass

    if not lib.base.coe(lib.base.match_range(crit_count, args.CRIT_COUNT)):
        state = STATE_CRIT
    elif not lib.base.coe(lib.base.match_range(warn_count, args.WARN_COUNT)):
        state = STATE_WARN
    else:
        state = STATE_OK


    if file_msg.count('\n') + 1 > 10:
        # shorten the message
        file_msg = file_msg.split('\n')
        file_msg = file_msg[0:5] + ['* ...'] + file_msg[-5:]
        file_msg = '\n'.join(file_msg)

    if state == STATE_OK:
        if warn_count == 0 and crit_count == 0:
            msg = 'Everything is ok. {} {} checked, all within the specified count and time range.\n\n'.format(
                file_count,
                lib.txt.pluralize("item", file_count),
            )
        else:
            msg = 'Everything is ok. {} {} checked. All within the specified count range, but {} outside "{}" time range, and {} outside "{}" time range.\n\n'.format(
                file_count,
                lib.txt.pluralize("item", file_count),
                warn_count,
                lib.human.seconds2human(args.WARN) if isinstance(lib.base.guess_type(args.WARN), int) else args.WARN,
                crit_count,
                lib.human.seconds2human(args.CRIT) if isinstance(lib.base.guess_type(args.CRIT), int) else args.CRIT,
            ) + file_msg
    else:
        msg = '{} {} outside count range "{}" and outside "{}" time range. {} {} outside count range "{}" and outside "{}" time range. {} {} checked. \n\n'.format(
            warn_count,
            lib.txt.pluralize("item", warn_count),
            args.WARN_COUNT,
            lib.human.seconds2human(args.WARN) if isinstance(lib.base.guess_type(args.WARN), int) else args.WARN,
            crit_count,
            lib.txt.pluralize("item", crit_count),
            args.CRIT_COUNT,
            lib.human.seconds2human(args.CRIT) if isinstance(lib.base.guess_type(args.CRIT), int) else args.CRIT,
            file_count,
            lib.txt.pluralize("item", file_count),
        ) + file_msg

    # calc perfdata if requested
    perfdata = ''
    if args.PERFDATA_MODE == 'mean' and len(ages) > 0:
        perfdata += lib.base.get_perfdata(label='mean-ages', value=round(float(sum(ages) / len(ages)), 3), uom='s')
    elif args.PERFDATA_MODE == 'median' and len(ages) > 0:
        ages_sorted = sorted(ages)
        middle_index = int(len(ages_sorted) / 2)
        # differentiate even / uneven list
        if len(ages_sorted) % 2 == 0:
            # even
            median = float((ages_sorted[middle_index] + ages_sorted[middle_index - 1]) / 2)
        else:
            # uneven (manually "flooring" it up)
            median = float(ages_sorted[int(middle_index + 0.5)])
        perfdata += lib.base.get_perfdata(label='median-ages', value=round(median, 3), uom='s')

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


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