#!/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 glob  # pylint: disable=C0413
import os  # pylint: disable=C0413
import sys  # pylint: disable=C0413

import lib.args  # 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
    PYTHON_MOD = e.name
import lib.lftest  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

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

DESCRIPTION = 'Checks the size for a file (in bytes).'


DEFAULT_CRIT = '1G'
DEFAULT_PATTERN = '*'
DEFAULT_TIMEOUT = 3
DEFAULT_WARN = '25M'


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='Threshold for the file size in a human readable format '
             '(base is always 1024; valid qualifiers are '
             'b, k/kb/kib, m/mb/mib, g/gb/gib etc.). '
             'Supports Nagios ranges. '
             'Example: `:1G` alerts if size is greater than 1 GiB.'
             'Default: %(default)s',
        dest='CRIT',
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--filename',
        help='File name to check. Supports glob in accordance with '
             'https://docs.python.org/2.7/library/glob.html. Note that using recursive globs '
             'can cause high memory usage. '
             'This is mutually exclusive with `-u` / `--url`.',
        dest='FILENAME',
    )

    parser.add_argument(
        '--pattern',
        help='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(
        '--password',
        help='SMB Password.',
        dest='PASSWORD',
    )

    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='Set the url of the file 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='Threshold for the file size in a human readable format '
             '(base is always 1024; valid qualifiers are '
             'b, k/kb/kib, m/mb/mib, g/gb/gib etc.). '
             'Supports Nagios ranges. '
             'Example: `:1G` alerts if size is greater than 1 GiB.'
             'Default: %(default)s',
        dest='WARN',
        default=DEFAULT_WARN,
    )

    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` parameters are mutually exclusive. Please use only one.') # pylint: disable=C0301

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

    # convert human readable nagios ranges to something that the Linuxfabrik libraries
    # can understand
    CRIT = lib.human.humanrange2bytes(args.CRIT)
    WARN = lib.human.humanrange2bytes(args.WARN)

    # fetch data from local
    if args.FILENAME:
        for item in sorted(glob.iglob(args.FILENAME)):
            # ignoring directories as the size of a directory is not consistently defined across
            # filesystems, and never is the size of the contents
            if os.path.isdir(item):
                continue
            table_data.append({
                'filename': item,
                'size': os.stat(item).st_size,
            })

    # or fetch data from remote
    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':
            if not HAVE_SMB:
                lib.base.cu('Python module "{}" is not installed.'.format(PYTHON_MOD)) # pylint: disable=E0601
            for item in lib.base.coe(lib.smb.glob(url, args.USERNAME, args.PASSWORD, args.TIMEOUT, pattern=args.PATTERN)): # pylint: disable=C0301
                # ignoring directories as the size of a directory is not consistently defined across
                # filesystems, and never is the size of the contents
                if item.is_dir():
                    continue
                table_data.append({
                    'filename': item,
                    'size': item.stat().st_size,
                })
        else:
            lib.base.cu('The protocol "{}" is not supported.'.format(proto))

    if len(table_data) == 0:
        msg = 'No files found.'
        lib.base.oao(msg, STATE_UNKNOWN, always_ok=args.ALWAYS_OK)

    # analyze data
    for i, item in enumerate(table_data):
        table_data[i]['size_hr'] = lib.human.bytes2human(table_data[i]['size'])
        table_data[i]['state'] = lib.base.get_state(
            item['size'],
            WARN,
            CRIT,
            _operator='range',
        )
        table_data[i]['state_hr'] = lib.base.state2str(table_data[i]['state'], empty_ok=False)
        if table_data[i]['state']:
            alert_count += 1
        state = lib.base.get_worst(state, table_data[i]['state'])

    # build the message
    msg += '{} {} checked. '.format(
        len(table_data),
        lib.txt.pluralize('file', len(table_data)),
    )
    if state == STATE_OK:
        msg += '{} within the given size thresholds ({}/{}).'.format(
            lib.txt.pluralize('', len(table_data), 'It is,All are'),
            args.WARN,
            args.CRIT,
        )
    else:
        msg += '{} {} outside the given size thresholds ({}/{}).'.format(
            alert_count,
            lib.txt.pluralize('', alert_count, 'is,are'),
            args.WARN,
            args.CRIT,
        )
    if len(table_data) == 1:
        # show info on first line when there is only one hit
        msg += ' Checked {}: {} {}'.format(
            table_data[0]['filename'],
            table_data[0]['size_hr'],
            lib.base.state2str(table_data[0]['state'], prefix=' '),
        )
    else:
        msg += '\n\n' + lib.base.get_table(
            table_data,
            [
                'filename',
                'size_hr',
                'state_hr',
            ],
            header=[
                'File',
                'Size',
                'State',
            ],
        )

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


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