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

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

missing_lib = None
try:
    import lib.smb

    HAVE_SMB = True
except ModuleNotFoundError as e:
    HAVE_SMB = False
    missing_lib = e.name

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

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."""

DEFAULT_PATTERN = '*'
DEFAULT_TIMEOUT = 3


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(
        '--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(
        '--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)

    # fetch data
    if args.FILENAME and args.URL:
        lib.base.cu(
            'The --filename and -u / --url parameter are mutually exclusive. Please only use one.'
        )

    now = lib.time.now()

    # init some vars
    msg = ''
    file_count = 0
    exceeded_threshold = False

    # Determine if we can break early for performance
    early_break_at = get_early_break_threshold(args.WARN, args.CRIT)

    if args.FILENAME:
        path = Path(args.FILENAME)
        for item in Path(path.anchor).glob(str(path.relative_to(path.anchor))):
            # Get stat info once to avoid multiple syscalls
            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

    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':
            if not HAVE_SMB:
                lib.base.cu(f'Python module "{missing_lib}" is not installed.')
            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
        else:
            lib.base.cu(f'The protocol "{proto}" is not supported.')

    if not lib.base.coe(lib.base.match_range(file_count, args.CRIT)):
        state = STATE_CRIT
    elif not lib.base.coe(lib.base.match_range(file_count, args.WARN)):
        state = STATE_WARN
    else:
        state = STATE_OK

    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)} (thresholds {args.WARN}/{args.CRIT})'
        )
    else:
        msg = (
            f'Found {file_count} matching '
            f'{lib.txt.pluralize("file", file_count)} (thresholds {args.WARN}/{args.CRIT})'
        )

    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)


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