#!/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 re
import sys
from collections import namedtuple

import lib.args
import lib.base
import lib.human
import lib.lftest
import lib.psutil
import lib.task
import lib.txt
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

try:
    import psutil
except ImportError:
    print('Python module "psutil" is not installed.')
    sys.exit(STATE_UNKNOWN)

# The fields this plugin uses out of what psutil reports about a filesystem's usage,
# as a namedtuple of its own. psutil's own tuples are not reused, because their shape
# moves between releases; the partition tuple comes from lib.psutil for the same reason.
sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent'])


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

DESCRIPTION = """Checks used or free disk space for each mounted partition. By default, only physical
devices are checked (hard disks, USB drives), ignoring pseudo and memory filesystems.
Supports filtering by mountpoint pattern or filesystem type. Thresholds can be set as
percentages or absolute values, and can target either used or free space, globally or
per mountpoint via --mount. On systems with many filesystems (hundreds of mounts),
--brief hides rows that are within the thresholds so the table only shows the
filesystems in WARN/CRIT state. Note that on ext2/3/4 filesystems, about 5% of disk
space is reserved for root by default and is not reflected in the available space shown
to regular users.
Alerts when usage exceeds the configured thresholds."""

DEFAULT_WARN = '90%USED'
DEFAULT_CRIT = '95%USED'
DEFAULT_NO_MATCH_SEVERITY = 'ok'
DEFAULT_TIMEOUT = 8
DEFAULT_UNREACHABLE_SEVERITY = 'warn'

# What lib.task reports for a filesystem that never came back, as opposed to one that
# answered with an error. The two are different findings, so the rows are compared
# against it to tell a filesystem that is unreachable from one that is merely unreadable.
TIMEOUT_MESSAGE = lib.task.TIMEOUT


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(
        '--brief',
        help='Hide table rows for filesystems within the thresholds and show only '
        'those in WARN/CRIT state. Perfdata and alerting are unaffected: all '
        'filesystems still emit perfdata and still drive the overall check state.',
        dest='BRIEF',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c',
        '--critical',
        help='CRIT threshold in the form `<number>[unit][method]`. '
        'Unit is one of `%%|K|M|G|T|P` (default: `%%`). `K` means kibibyte etc. '
        'Method is one of `USED|FREE` (default: `USED`). '
        '`USED` means "number or more", `FREE` means "number or less". '
        'Examples: `95` = 95%% used. `9.5M` = 9.5 MiB used. `5%%FREE`. `1400GUSED`. '
        'Default: %(default)s',
        dest='CRIT',
        type=lib.args.number_unit_method,
        default=DEFAULT_CRIT,
    )

    # Deprecated filter aliases, superseded by --match / --ignore. Kept
    # (hidden) so existing service definitions keep working: --include-*
    # feed the same list as --match, --exclude-* the same list as --ignore.
    parser.add_argument(
        '--exclude-pattern',
        help=argparse.SUPPRESS,
        dest='EXCLUDE_PATTERN',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--exclude-regex',
        help=argparse.SUPPRESS,
        dest='EXCLUDE_REGEX',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--fstype',
        help='Override the default behaviour (check physical devices only) and check these '
        'file system types instead. '
        'Can be specified multiple times. '
        'Run `disk-usage --list-fstypes` first to see available types (they are machine dependent).',
        dest='FSTYPE',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--ignore',
        help='Ignore mountpoints matching this Python regular expression. '
        'Case-insensitive by default (on Windows, drive letters and paths are '
        'case-insensitive; use drive letters such as `C:` or `C`). '
        'For case-sensitive matching, wrap the pattern in `(?-i:...)`, e.g. `(?-i:Data)`. '
        'Can be specified multiple times.',
        dest='IGNORE',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--include-pattern',
        help=argparse.SUPPRESS,
        dest='INCLUDE_PATTERN',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--include-regex',
        help=argparse.SUPPRESS,
        dest='INCLUDE_REGEX',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--list-fstypes',
        help='Print available file system types and which ones are checked by default, then exit.',
        dest='LIST_FSTYPES',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--match',
        help='Only check mountpoints matching this Python regular expression. '
        'Case-insensitive by default (on Windows, drive letters and paths are '
        'case-insensitive; use drive letters such as `C:` or `C`). '
        'For case-sensitive matching, wrap the pattern in `(?-i:...)`, e.g. `(?-i:Data)`. '
        'Can be specified multiple times. ' + lib.args.MATCH_IGNORE_PRECEDENCE,
        dest='MATCH',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--mount',
        help='Override the global --warning/--critical thresholds for a single mountpoint, '
        'in the form `<mountpoint>,<warning>,<critical>`. '
        'Each threshold uses the same `<number>[unit][method]` syntax as --warning/--critical. '
        'The mountpoint is matched exactly and case-insensitively, so the override always '
        'hits exactly one mountpoint and never several. '
        'On Windows, use drive letters such as `C:` or `C`. '
        'Can be specified multiple times. '
        'Example: `--mount=/var/log,80%%USED,90%%USED`',
        dest='MOUNT',
        type=lib.args.csv,
        action='append',
        default=None,
    )

    parser.add_argument(
        '--no-match-severity',
        help=lib.args.help('--no-match-severity') + ' Default: %(default)s',
        dest='NO_MATCH_SEVERITY',
        choices=['ok', 'warn', 'crit', 'unknown'],
        default=DEFAULT_NO_MATCH_SEVERITY,
    )

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

    parser.add_argument(
        '--perfdata-regex',
        help='Only emit perfdata keys matching this Python regex. '
        'For a list of perfdata keys, see the README or run this plugin. '
        'Can be specified multiple times.',
        action='append',
        dest='PERFDATA_REGEX',
        default=None,
    )

    parser.add_argument(
        '--test',
        help=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

    parser.add_argument(
        '--timeout',
        help='How long a filesystem gets to answer before it is reported as '
        'unreachable. Only a network filesystem ever needs it: one whose server has '
        'stopped answering does not fail, it blocks. Every filesystem is asked at the '
        'same time and they share one deadline, so this is the runtime of the whole '
        'check and not a budget per filesystem. '
        'Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    parser.add_argument(
        '--unreachable-severity',
        help='State to report for a filesystem that does not answer within --timeout. '
        'A filesystem that answers with an error is a different case: it stays at OK '
        'and is listed as `N/A`, because a mount point the monitoring user may not read '
        'says nothing about the health of the host. '
        'Default: %(default)s',
        dest='UNREACHABLE_SEVERITY',
        choices=['ok', 'warn', 'crit', 'unknown'],
        default=DEFAULT_UNREACHABLE_SEVERITY,
    )

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold in the form `<number>[unit][method]`. '
        'Unit is one of `%%|K|M|G|T|P` (default: `%%`). `K` means kibibyte etc. '
        'Method is one of `USED|FREE` (default: `USED`). '
        '`USED` means "number or more", `FREE` means "number or less". '
        'Examples: `95` = 95%% used. `9.5M` = 9.5 MiB used. `5%%FREE`. `1400GUSED`. '
        'Default: %(default)s',
        dest='WARN',
        type=lib.args.number_unit_method,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


def _load_disk_usage_fixture(raw_json):
    """Convert a test fixture into the two data structures the plugin
    expects from `psutil.disk_partitions()` and
    `psutil.disk_usage(mountpoint)`:

    - `partitions`: a list of `sdiskpart(device, mountpoint, fstype, opts)`
      namedtuples
    - `usage`: a dict keyed by mountpoint, each value an
      `sdiskusage(total, used, free, percent)` namedtuple
    - `timeouts`: the mountpoints that never answer, which needs a real filesystem
      whose server has gone away and is therefore named in the fixture instead

    A mountpoint that appears in neither `usage` nor `timeouts` stands for one the
    plugin cannot read, for example a volume that requires root.

    Fixture shape:

        {
          "partitions": [
            {"device": "/dev/vda1", "mountpoint": "/", "fstype": "ext4",
             "opts": "rw,relatime"},
            ...
          ],
          "usage": {
            "/": {"total": <bytes>, "used": <bytes>, "free": <bytes>,
                  "percent": <0..100>},
            ...
          },
          "timeouts": ["/mnt/nfs"]
        }
    """
    data = json.loads(raw_json)
    partitions = [
        lib.psutil.sdiskpart(
            p['device'],
            p['mountpoint'],
            p.get('fstype', ''),
            p.get('opts', ''),
        )
        for p in data.get('partitions', [])
    ]
    usage = {
        mp: sdiskusage(u['total'], u['used'], u['free'], u['percent'])
        for mp, u in data.get('usage', {}).items()
    }
    timeouts = set(data.get('timeouts', []))
    return partitions, usage, timeouts


def get_usage(mountpoints, timeout):
    """Return the usage of every mount point, as a dict keyed by mount point, each value
    a `(True, sdiskusage)` or `(False, errormessage)` pair.

    A filesystem whose server has stopped answering does not fail, it blocks, and it
    blocks in a sleep inside the kernel that no timeout inside this process can cut
    short. lib.task asks every mount point in a process of its own, all of them at the
    same time and under one shared deadline, and disposes of the ones that miss it, so
    several mounts of the same missing server cost one `--timeout` between them and not
    one each.
    """
    if not mountpoints:
        return {}
    answers = lib.task.run_each(
        [
            (mountpoint, lambda mp=mountpoint: list(psutil.disk_usage(mp)))
            for mountpoint in mountpoints
        ],
        timeout,
    )
    result = {}
    for mountpoint, (success, value) in answers.items():
        result[mountpoint] = (True, sdiskusage(*value)) if success else (False, value)
    return result


def list_fstypes():
    """Print a nice table showing which file system types exist and which are checked by default."""
    # get all partitions, no matter which ones
    parts = lib.psutil.get_partitions(include_all=True)
    table_data = []
    for part in parts:
        table_data.append(
            {
                'fstype': part.fstype,
                'mountpoint': part.mountpoint,
                'device': part.device,
                'checked': False,
            }
        )

    # get which ones are checked by default
    parts = lib.psutil.get_partitions()
    for i, item in enumerate(table_data):
        for part in parts:
            if part.mountpoint == item['mountpoint']:
                table_data[i]['checked'] = True
                continue

    # sort table by fstype, mountpoint
    keys = ['fstype', 'mountpoint', 'device', 'checked']
    lib.base.oao(
        lib.base.get_table(table_data, keys, header=keys, sort_by_key='fstype'),
        STATE_OK,
    )


def normalize_mountpoint(mountpoint):
    """Normalize a mountpoint for exact, case-insensitive matching. Lowercases,
    turns backslashes into slashes, expands a bare Windows drive letter
    (`c` -> `c:`), and strips a trailing slash (except for root `/`), so that
    `C`, `C:` and `C:\\` all match the mountpoint `C:\\`.
    """
    mp = mountpoint.strip().lower().replace('\\', '/')
    if len(mp) == 1 and mp.isalpha():
        mp = f'{mp}:'
    if len(mp) > 1:
        mp = mp.rstrip('/')
    return mp


def _threshold_operands(usage, threshold):
    """Map a `(number, unit, method)` threshold triple (as produced by
    `lib.args.number_unit_method`) and a filesystem's usage onto the
    `(value, limit, operator)` arguments for `lib.base.get_state()`:

    - `USED` compares the used amount and alerts when it is "number or more"
      (`ge`); `FREE` compares the free amount and alerts when it is "number or
      less" (`le`).
    - a `%` unit compares percentages, any other unit compares absolute bytes.
    """
    number, unit, method = threshold
    if unit == '%':
        value = usage.percent if method == 'USED' else 100.0 - usage.percent
        limit = number
    else:
        value = usage.used if method == 'USED' else usage.free
        limit = lib.human.human2bytes(''.join(threshold[:2]))
    operator = 'ge' if method == 'USED' else 'le'
    return value, limit, operator


def _perfdata_thresholds(usage, threshold):
    """Map a `(number, unit, method)` threshold triple onto the numeric warn or
    crit line for the two threshold-bearing perfdata fields, mirroring how
    `evaluate_disk_state()` compares. Returns `(percent_limit, usage_limit)`,
    where each is the value to write into the respective perfdata field or
    `None` when the threshold does not apply to that field:

    - a `%` threshold drives the `-percent` field (used %); `USED` puts the
      line at `number`, `FREE` at `100 - number` (the used-% at which free hits
      the limit).
    - an absolute (byte) threshold drives the `-usage` field (used bytes);
      `USED` puts the line at `number` bytes, `FREE` at `total - number` bytes.

    A `FREE` threshold larger than the filesystem itself (`5GFREE` on a 1 GiB
    `/boot`) inverts to a negative line, which no graphing tool can draw. Such a
    filesystem alerts no matter how little is used, so the line is pinned to 0:
    every byte of usage is on the alerting side of it.
    """
    number, unit, method = threshold
    if unit == '%':
        percent_limit = float(number) if method == 'USED' else 100.0 - float(number)
        return max(percent_limit, 0.0), None
    limit_bytes = lib.human.human2bytes(''.join(threshold[:2]))
    usage_limit = limit_bytes if method == 'USED' else usage.total - limit_bytes
    return None, max(usage_limit, 0)


def evaluate_disk_state(usage, warn, crit):
    """Return the WARN/CRIT state for one filesystem. `warn` and `crit` are
    `(number, unit, method)` triples as produced by
    `lib.args.number_unit_method`. Checks WARN first, then CRIT, and combines
    them with get_worst().
    """
    warn_value, warn_limit, warn_op = _threshold_operands(usage, warn)
    crit_value, crit_limit, crit_op = _threshold_operands(usage, crit)
    disk_state = lib.base.get_state(warn_value, warn_limit, None, warn_op)
    return lib.base.get_worst(
        disk_state,
        lib.base.get_state(crit_value, None, crit_limit, crit_op),
    )


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)

    # set default values for append parameters that were not specified
    if args.EXCLUDE_PATTERN is None:
        args.EXCLUDE_PATTERN = []
    if args.EXCLUDE_REGEX is None:
        args.EXCLUDE_REGEX = []
    if args.FSTYPE is None:
        args.FSTYPE = []
    if args.IGNORE is None:
        args.IGNORE = []
    if args.INCLUDE_PATTERN is None:
        args.INCLUDE_PATTERN = []
    if args.INCLUDE_REGEX is None:
        args.INCLUDE_REGEX = []
    if args.MATCH is None:
        args.MATCH = []
    if args.MOUNT is None:
        args.MOUNT = []
    if args.PERFDATA_REGEX is None:
        args.PERFDATA_REGEX = []

    # args.WARN[0] = number, args.WARN[1] = unit, args.WARN[2] = USED|FREE
    try:
        float(args.WARN[0])
        float(args.CRIT[0])
    except ValueError:
        lib.base.cu('Invalid parameter value.')

    # --mount: per-mountpoint threshold overrides. Build a lookup keyed by the
    # normalized mountpoint; each value is a (warn, crit) pair of
    # number_unit_method triples that override the global thresholds for that
    # one mountpoint. mount_inputs keeps the mountpoint as the user typed it,
    # for the "matched no checked filesystem" note below.
    mount_overrides = {}
    mount_inputs = {}
    for entry in args.MOUNT:
        if len(entry) != 3:
            lib.base.cu(
                f'`--mount` needs `<mountpoint>,<warning>,<critical>`, got: {",".join(entry)}'
            )
        mountpoint, warn_raw, crit_raw = entry
        warn = lib.args.number_unit_method(warn_raw.strip())
        crit = lib.args.number_unit_method(crit_raw.strip())
        try:
            float(warn[0])
            float(crit[0])
        except ValueError:
            lib.base.cu(f'Invalid `--mount` threshold for "{mountpoint}".')
        mount_key = normalize_mountpoint(mountpoint)
        mount_overrides[mount_key] = (warn, crit)
        mount_inputs[mount_key] = mountpoint.strip()

    # show partition information and exit
    if args.LIST_FSTYPES:
        list_fstypes()

    # fetch data
    fixture_usage = None
    fixture_timeouts = set()
    if args.TEST is None:
        # `--fstype` means the user wants to pick the file system types themselves, so
        # everything is listed; the default behaviour is to check physical devices only
        # (e.g. hard disks, cd-rom drives, USB keys) and ignore all others (e.g. pseudo,
        # memory, duplicate, inaccessible filesystems).
        parts = lib.psutil.get_partitions(include_all=bool(args.FSTYPE))
    else:
        stdout, _, _ = lib.lftest.test(args.TEST)
        parts, fixture_usage, fixture_timeouts = _load_disk_usage_fixture(stdout)

    # init some vars
    state = STATE_OK
    perfdata = ''
    table_data = []
    seen_overrides = set()  # which --mount keys actually matched a checked filesystem
    # --match / --ignore are the canonical filter names. Unlike the generic
    # case-sensitive --match/--ignore convention used elsewhere, disk-usage
    # filters case-insensitively because filesystem paths and Windows drive
    # letters are case-insensitive. The deprecated --include-regex /
    # --exclude-regex aliases feed the same lists.
    compiled_include_regex = [
        lib.base.coe(p)
        for p in lib.txt.compile_regex(
            args.INCLUDE_REGEX + args.MATCH, '--match', flags=re.IGNORECASE
        )
    ]
    compiled_exclude_regex = [
        lib.base.coe(p)
        for p in lib.txt.compile_regex(
            args.EXCLUDE_REGEX + args.IGNORE, '--ignore', flags=re.IGNORECASE
        )
    ]
    compiled_perfdata_regex = [
        lib.base.coe(p)
        for p in lib.txt.compile_regex(
            args.PERFDATA_REGEX, '--perfdata-regex', flags=re.IGNORECASE
        )
    ]

    # analyze data
    # Filter first, then ask the filesystems that are left. That order is what makes
    # `--ignore` a way out of a mount that is known to be unreachable: one that is
    # excluded is never asked and cannot cost the check its deadline.
    selected = []
    for part in parts:
        # sdiskpart(device='/dev/vda2', mountpoint='/', fstype='ext4', opts='rw,relatime')
        # sdiskpart(
        #   device='/dev/sr0',
        #   mountpoint='/run/media/root/CentOS 7 x86_64',
        #   fstype='iso9660',
        #   opts='ro,nosuid,nodev,relatime,uid=0,gid=0,iocharset=utf8,mode=0400,dmode=0500'
        # )
        # ignore `/snap`, iso mountpoints and cdroms (UDF = universal disk format)
        if args.FSTYPE:
            # user wants to check file system types on his own
            if part.fstype not in args.FSTYPE:
                continue
        else:
            # default behaviour - ignore read-only and some other filesystems
            if part.fstype in ['CDFS', 'iso9660', 'squashfs', 'UDF'] or part.opts in [
                'cdrom'
            ]:
                continue

        # Filter mountpoints. --match (include) is applied first, then
        # --ignore (exclude), so a mountpoint hit by --ignore is dropped even
        # if it also matches --match. Matching is case-insensitive.
        # hint: we can't do `if not part or part.mountpoint in args.IGNORE:` because it is
        # impossible to specify a "Y:\" on the command line ('Y:\' or 'Y:\\' all don't work).
        # The regexes are matched against the mountpoint as-is: they already carry
        # re.IGNORECASE, and lowercasing the subject as well would defeat a
        # `(?-i:...)` group that opts back into case-sensitive matching. Only the
        # deprecated substring aliases lowercase both sides themselves.
        mountpoint = part.mountpoint
        mountpoint_lower = mountpoint.lower()
        if args.INCLUDE_PATTERN or args.INCLUDE_REGEX or args.MATCH:
            if not any(
                include_pattern.lower() in mountpoint_lower
                for include_pattern in args.INCLUDE_PATTERN
            ) and not any(item.search(mountpoint) for item in compiled_include_regex):
                continue
        if args.EXCLUDE_PATTERN or args.EXCLUDE_REGEX or args.IGNORE:
            if any(
                exclude_pattern.lower() in mountpoint_lower
                for exclude_pattern in args.EXCLUDE_PATTERN
            ) or any(item.search(mountpoint) for item in compiled_exclude_regex):
                continue

        selected.append(part)

    # Ask every filesystem that survived the filters, all of them at the same time and
    # under one shared deadline, so that several mounts of a server that went away cost
    # the check one --timeout and not one each.
    measured = (
        {}
        if fixture_usage is not None
        else get_usage([part.mountpoint for part in selected], args.TIMEOUT)
    )

    for part in selected:
        if fixture_usage is not None:
            success = part.mountpoint in fixture_usage
            usage = fixture_usage.get(part.mountpoint)
            failure = (
                TIMEOUT_MESSAGE
                if part.mountpoint in fixture_timeouts
                else 'is not readable'
            )
        else:
            success, result = measured[part.mountpoint]
            usage = result if success else None
            failure = result if not success else ''
        if not success:
            # A filesystem that answered with an error is unreadable, which is what a
            # Kubernetes CSI volume below /var/lib/kubelet looks like to a non-root
            # user, and it has never alerted. One that did not answer at all is a
            # different finding: its server is gone, and every process touching that
            # mount point is stuck, so it alerts unless --unreachable-severity says
            # otherwise.
            if failure == TIMEOUT_MESSAGE:
                row_state = lib.base.str2state(args.UNREACHABLE_SEVERITY)
                state = lib.base.get_worst(state, row_state)
                percent_cell = (
                    f'{TIMEOUT_MESSAGE}{lib.base.state2str(row_state, prefix=" ")}'
                )
            else:
                row_state = STATE_OK
                percent_cell = 'N/A'
            table_data.append(
                {
                    'mountpoint': f'{part.mountpoint}',
                    'type': f'{part.fstype}',
                    'used': 'N/A',
                    'avail': 'N/A',
                    'size': 'N/A',
                    'percent': percent_cell,
                    # unreadable filesystems do not alert and sort to the
                    # bottom of the usage-sorted table (see get_table below).
                    '_state': row_state,
                    '_override': False,
                    '_percent': -1.0,
                }
            )
            continue

        # evaluate WARN/CRIT, using a --mount per-mountpoint override if one
        # matches this mountpoint, otherwise the global thresholds
        mount_key = normalize_mountpoint(part.mountpoint)
        is_override = mount_key in mount_overrides
        if is_override:
            seen_overrides.add(mount_key)
        warn, crit = mount_overrides.get(mount_key, (args.WARN, args.CRIT))
        disk_state = evaluate_disk_state(usage, warn, crit)
        state = lib.base.get_worst(state, disk_state)

        # Map the effective thresholds onto the perfdata fields they apply to,
        # so graphing tools can draw warn/crit lines. A percentage threshold
        # lands on `-percent`, an absolute (byte) threshold on `-usage`; the
        # other field stays unthresholded.
        warn_percent, warn_usage = _perfdata_thresholds(usage, warn)
        crit_percent, crit_usage = _perfdata_thresholds(usage, crit)

        # Use% cell: the percentage, the per-mountpoint thresholds when --mount
        # overrides this row (so the table explains why an otherwise-fine row
        # alerts), and the state marker last (kept at the end of the cell for
        # IcingaWeb, which turns it into an icon).
        percent_cell = f'{usage.percent}%'
        if is_override:
            percent_cell += f' (warn={"".join(warn)} crit={"".join(crit)})'
        percent_cell += lib.base.state2str(disk_state, prefix=' ')

        perfdata_key = f'{part.mountpoint}-usage'
        if not args.PERFDATA_REGEX or any(
            item.search(perfdata_key) for item in compiled_perfdata_regex
        ):
            perfdata += lib.base.get_perfdata(
                perfdata_key,
                usage.used,
                uom='B',
                warn=warn_usage,
                crit=crit_usage,
                _min=0,
                _max=usage.total,
            )
        perfdata_key = f'{part.mountpoint}-total'
        if not args.PERFDATA_REGEX or any(
            item.search(perfdata_key) for item in compiled_perfdata_regex
        ):
            perfdata += lib.base.get_perfdata(
                perfdata_key,
                usage.total,
                uom='B',
                warn=None,
                crit=None,
                _min=0,
                _max=usage.total,
            )
        perfdata_key = f'{part.mountpoint}-percent'
        if not args.PERFDATA_REGEX or any(
            item.search(perfdata_key) for item in compiled_perfdata_regex
        ):
            perfdata += lib.base.get_perfdata(
                perfdata_key,
                usage.percent,
                uom='%',
                warn=warn_percent,
                crit=crit_percent,
                _min=0,
                _max=100,
            )
        table_data.append(
            {
                'mountpoint': f'{part.mountpoint}',
                'type': f'{part.fstype}',
                'used': lib.human.bytes2human(usage.used),
                'avail': lib.human.bytes2human(usage.free),
                'size': lib.human.bytes2human(usage.total),
                'percent': percent_cell,
                # internal per-row fields, not rendered because get_table()
                # uses an explicit column whitelist: per-row state for the
                # --brief filter below, whether a --mount override applied
                # (so the single-mountpoint summary does not repeat the
                # thresholds the Use% value already carries), and the numeric
                # usage percentage the table is sorted by.
                '_state': disk_state,
                '_override': is_override,
                '_percent': usage.percent,
            }
        )

    # Filter table rows for --brief display: hide rows within the
    # thresholds and keep only WARN/CRIT rows. Perfdata and alerting stay
    # untouched above this point; --brief only reshapes the human-readable
    # output.
    if args.BRIEF:
        display_rows = [
            row for row in table_data if row.get('_state', STATE_OK) != STATE_OK
        ]
    else:
        display_rows = table_data

    # build the message
    thresholds = f'warn={"".join(args.WARN)} crit={"".join(args.CRIT)}'
    if not table_data:
        msg = 'Nothing checked.'
        state = lib.base.str2state(args.NO_MATCH_SEVERITY)
    elif args.BRIEF and not display_rows:
        # --brief and nothing worth showing: one-line summary only.
        msg = f'Everything is ok. ({thresholds})'
    elif len(table_data) == 1 and not args.BRIEF:
        # single matched filesystem (no --brief): full single-line summary with
        # the values inline. A --mount override already carries its thresholds
        # in the Use% value, so only append the global thresholds otherwise.
        row = table_data[0]
        msg = (
            f'{row["mountpoint"]}'
            f' {row["percent"]}'
            f' - total: {row["size"]}'
            f', free: {row["avail"]}'
            f', used: {row["used"]}'
        )
        if not row.get('_override'):
            msg += f' ({thresholds})'
    else:
        if state == STATE_CRIT:
            header = 'There are critical errors.'
        elif state == STATE_WARN:
            header = 'There are warnings.'
        else:
            header = 'Everything is ok.'
        table = lib.base.get_table(
            display_rows,
            ['mountpoint', 'type', 'size', 'used', 'avail', 'percent'],
            header=['Mountpoint', 'Type', 'Size', 'Used', 'Avail', 'Use%'],
            sort_by_key='_percent',
            sort_order_reverse=True,
        )
        msg = f'{header} ({thresholds})\n\n{table}'

    # warn (in the output, not in the state) about --mount entries that did not
    # match any checked filesystem, so a typo or a filesystem that is not
    # checked by default does not silently disable an intended threshold. The
    # note goes on the first line (not the long output) so it is visible in the
    # web UI and in notifications without expanding the output.
    unmatched = sorted(
        mount_inputs[key] for key in mount_overrides if key not in seen_overrides
    )
    if unmatched:
        note = 'ignored `--mount` for ' + ', '.join(unmatched) + ' (not checked)'
        head, separator, tail = msg.partition('\n')
        msg = f'{head} {note}{separator}{tail}'

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