#!/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 stat
import sys

import lib.args
import lib.base
import lib.disk
import lib.smb
import lib.time
import lib.txt
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Counts the number of files matching a glob pattern and alerts when the count exceeds
the configured thresholds. Can filter by modification time range, restrict to files
or directories only, and supports SMB shares.
Reads only the file metadata, never the contents. The plugin is not shipped in the
sudoers allowlist, so it can only see files the monitoring user may read; see
PLUGINS-FILE.md for what to do about a file it cannot access."""

DEFAULT_PATTERN = '*'
DEFAULT_TIMEOUT = 3


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(
        '-c',
        '--critical',
        help='CRIT threshold for the number of matching files.',
        dest='CRIT',
    )

    parser.add_argument(
        '--filename',
        help='File or directory name to check (supports glob patterns). '
        'Beware of recursive globs. '
        'Mutually exclusive with --url.',
        dest='FILENAME',
    )

    parser.add_argument(
        '--no-early-break',
        help='Count every matching file instead of stopping as soon as the count '
        'exceeds the thresholds. '
        'Makes the `file_count` perfdata exact, at the cost of runtime on '
        'directories with many files.',
        dest='NO_EARLY_BREAK',
        action='store_true',
        default=False,
    )

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

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

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

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

    parser.add_argument(
        '--pattern',
        help='SMB search pattern to match directory or file names. '
        'Use `*` for multiple characters and `?` for a single character. '
        'Does not support regex. '
        'Default: %(default)s',
        dest='PATTERN',
        default=DEFAULT_PATTERN,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    parser.add_argument(
        '--timerange',
        help='Only count files modified within this time range in seconds.',
        dest='TIMERANGE',
    )

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

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

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold for the number of matching files.',
        dest='WARN',
    )

    args, _ = parser.parse_known_args()
    return args


def get_early_break_threshold(warn, crit):
    """Extract a numeric threshold for early loop termination.
    Returns None if thresholds are too complex to parse.
    """
    thresholds = []
    for threshold in [warn, crit]:
        if not threshold:
            continue
        # Try to extract numeric value from simple threshold formats
        # Handles: "100", "10:100", ":100", "10:", "@10:100"
        threshold = threshold.lstrip('@')
        if ':' in threshold:
            parts = threshold.split(':')
            # Get the upper bound if it exists
            if parts[-1]:
                try:
                    thresholds.append(int(parts[-1]))
                except ValueError:
                    return None
        else:
            # Simple numeric threshold
            try:
                thresholds.append(int(threshold))
            except ValueError:
                return None

    if thresholds:
        # Use the maximum threshold + 1 to confirm we're "more than"
        return max(thresholds) + 1
    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)

    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(f'Could not parse the protocol of the url "{args.URL}".')
        proto, url = split_url
        if proto != 'smb':
            lib.base.cu(f'The protocol "{proto}" is not supported.')

    # fetch data
    now = lib.time.now()
    file_count = 0
    exceeded_threshold = False

    # Determine if we can break early for performance. Breaking early caps the
    # count, and with it the perfdata, at the highest threshold + 1.
    early_break_at = None
    if not args.NO_EARLY_BREAK:
        early_break_at = get_early_break_threshold(args.WARN, args.CRIT)

    if args.FILENAME:
        for item in lib.disk.glob(args.FILENAME):
            # Get stat info once to avoid multiple syscalls
            stat_info = lib.disk.stat(item)
            if stat_info is None:
                # Skip files we can't stat
                continue

            is_dir = stat.S_ISDIR(stat_info.st_mode)
            is_file = stat.S_ISREG(stat_info.st_mode)

            if is_file and args.ONLY_DIRS:
                continue
            if is_dir and args.ONLY_FILES:
                continue
            if args.TIMERANGE:
                age = now - stat_info.st_mtime
                if not lib.base.coe(lib.base.match_range(age, args.TIMERANGE)):
                    continue

            file_count += 1

            # Break early if we've exceeded the threshold because it makes no sense to count any
            # further, it just costs time and resources
            if early_break_at and file_count >= early_break_at:
                exceeded_threshold = True
                break

    if args.URL:
        if proto == 'smb':
            for item in lib.base.coe(
                lib.smb.glob(
                    url,
                    args.USERNAME,
                    args.PASSWORD,
                    args.TIMEOUT,
                    pattern=args.PATTERN,
                )
            ):
                # Get stat info once to avoid multiple calls
                try:
                    stat_info = item.stat()
                except (OSError, PermissionError):
                    # Skip files we can't stat
                    continue

                is_dir = stat.S_ISDIR(stat_info.st_mode)
                is_file = stat.S_ISREG(stat_info.st_mode)

                if is_file and args.ONLY_DIRS:
                    continue
                if is_dir and args.ONLY_FILES:
                    continue
                if args.TIMERANGE:
                    age = now - stat_info.st_mtime
                    if not lib.base.coe(lib.base.match_range(age, args.TIMERANGE)):
                        continue

                file_count += 1

                # Break early if we've exceeded the threshold because it makes no sense to count any
                # further, it just costs time and resources
                if early_break_at and file_count >= early_break_at:
                    exceeded_threshold = True
                    break

    # init some vars
    msg = ''
    state = STATE_OK

    # analyze data
    if not lib.base.coe(lib.base.match_range(file_count, args.WARN)):
        state = STATE_WARN
    if not lib.base.coe(lib.base.match_range(file_count, args.CRIT)):
        state = STATE_CRIT

    # build the message
    # Name the range the count broke, or the one it stayed within. Without a threshold
    # there is nothing to name, and printing "None" would read like a value the admin
    # set.
    if state == STATE_CRIT:
        spec, view = args.CRIT, 'alert'
    elif state == STATE_WARN:
        spec, view = args.WARN, 'alert'
    else:
        spec, view = (args.WARN if args.WARN is not None else args.CRIT), 'ok'
    condition = lib.base.coe(lib.base.range2txt(spec, view=view))
    if condition:
        condition = f', {condition}{lib.base.state2str(state, prefix=" ")}'

    if exceeded_threshold:
        # Report "more than" the threshold value we broke at
        display_count = file_count - 1
        msg = (
            f'Found more than {display_count} matching '
            f'{lib.txt.pluralize("file", display_count)}{condition}'
        )
    else:
        msg = (
            f'Found {file_count} matching '
            f'{lib.txt.pluralize("file", file_count)}{condition}'
        )

    perfdata = lib.base.get_perfdata(
        'file_count',
        file_count,
        warn=args.WARN,
        crit=args.CRIT,
        _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()
