#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.

from enum import auto, Enum
from typing import NamedTuple

# NOTE: Careful when replacing the *-import below with a more specific import. This can cause
# problems because it might remove variables from the check-context which are necessary for
# resolving legacy discovery results such as [("SUMMARY", "diskstat_default_levels")]. Furthermore,
# it might also remove variables needed for accessing discovery rulesets.
from cmk.base.check_legacy_includes.df import *  # pylint: disable=wildcard-import,unused-wildcard-import
from cmk.base.check_legacy_includes.size_trend import size_trend
from cmk.base.plugins.agent_based.utils.df import df_discovery, DfBlock

factory_settings["filesystem_default_levels"] = FILESYSTEM_DEFAULT_LEVELS


inventory_df_rules = []
inventory_df_exclude_fs = ["tmpfs", "nfs", "smbfs", "cifs", "iso9660"]


def _filter_df_blocks(df_blocks, inventory_options):
    ignore_fs_types = inventory_options.get("ignore_fs_types", inventory_df_exclude_fs)
    never_ignore_mountpoints = inventory_options.get("never_ignore_mountpoints", [])

    for df_block in df_blocks:
        if df_block.mountpoint in inventory_df_exclude_mountpoints:
            continue

        if df_block.mountpoint.startswith("/var/lib/docker/"):
            # Always exclude filesystems below dockers local storage area
            # and also exclude docker mounts in containers which are reported
            # by the agent when the agent is executed in the container context
            continue

        if df_block.fs_type not in ignore_fs_types:
            yield df_block
            continue

        if not _ignore_mountpoint(df_block.mountpoint, never_ignore_mountpoints):
            yield df_block
            continue


def _ignore_mountpoint(mountpoint, never_ignore_mountpoints):
    # Filesystem is not ignored, so check against mount point patterns
    for p in never_ignore_mountpoints:
        if p[0] == "~" and regex(p[1:]).match(mountpoint):
            return False

        if mountpoint == p:
            return False
    return True


class ItemBehaviour(Enum):
    default = auto()
    volume_name = auto()
    uuid = auto()


class ItemAndGrouping(NamedTuple):
    item: str
    grouping: str
    for_all: ItemBehaviour
    for_single: ItemBehaviour


def _get_item_behaviour_for_block_devices(params) -> ItemBehaviour:
    # The first one comes from WATO ruleset the second one is set during discovery
    # which is kind of normalized name.
    if params.get("mountpoint_for_block_devices") in ["uuid_as_mountpoint", "uuid"]:
        return ItemBehaviour.uuid
    return ItemBehaviour.volume_name


def _get_item_and_grouping(params) -> ItemAndGrouping:
    item = params.get("item_appearance", "mountpoint")
    grouping = params.get("grouping_behaviour", "mountpoint")

    for_all = ItemBehaviour.default
    for_single = ItemBehaviour.default
    if item == "volume_name_and_mountpoint":
        if grouping == "volume_name_and_mountpoint":
            for_all = ItemBehaviour.volume_name
        else:
            for_single = ItemBehaviour.volume_name

    elif item == "uuid_and_mountpoint":
        if grouping == "uuid_and_mountpoint":
            for_all = ItemBehaviour.uuid
        else:
            for_single = ItemBehaviour.uuid

    return ItemAndGrouping(
        item=item,
        grouping=grouping,
        for_all=for_all,
        for_single=for_single,
    )


def _prepare_item_name(entry, behaviour):
    if entry.device and behaviour == ItemBehaviour.volume_name:
        return "%s %s" % (entry.device, entry.mountpoint)

    if entry.uuid and behaviour == ItemBehaviour.uuid:
        return "%s %s" % (entry.uuid, entry.mountpoint)

    return entry.mountpoint


def _handle_block_devices(df_blocks, mountpoint_for_block_devices):
    # TODO What about df_inodes?
    # Not sure but it seems that inodes of btrfs FS are always zero (seen in our data pool):
    # /dev/sda1      btrfs         0     0      0     - /.snapshots
    # /dev/sda1      btrfs         0     0      0     - /var/tm
    # ...
    handled_df_blocks = []
    for df_block in df_blocks:
        if df_block.fs_type == "btrfs":
            # This particular bit of magic originated in Werk #2671 and has the purpose
            # of avoiding duplicate checks.
            # Compatibility: Before filtering/grouping/... we use '"btrfs " + device' as
            # mountpoint - regardless which field for mountpoint is set in df_section.

            if df_block.uuid and mountpoint_for_block_devices == ItemBehaviour.uuid:
                suffix = df_block.uuid
            else:  # mountpoint_for_block_devices == ItemBehaviour.volume_name
                suffix = df_block.device

            mountpoint = " ".join([df_block.fs_type, suffix])

            df_block = DfBlock(
                device=df_block.device,
                fs_type=df_block.fs_type,
                size_mb=df_block.size_mb,
                avail_mb=df_block.avail_mb,
                reserved_mb=df_block.reserved_mb,
                mountpoint=mountpoint,
                uuid=df_block.uuid,
            )

        handled_df_blocks.append(df_block)
    return handled_df_blocks


def inventory_df(parsed):
    inventory_options = host_extra_conf_merged(host_name(), inventory_df_rules)
    mountpoint_for_block_devices = _get_item_behaviour_for_block_devices(inventory_options)
    item_and_grouping = _get_item_and_grouping(inventory_options)

    df_blocks, _df_inodes = parsed
    df_blocks = _handle_block_devices(df_blocks, mountpoint_for_block_devices)

    filtered_blocks = _filter_df_blocks(df_blocks, inventory_options)

    mplist = [
        _prepare_item_name(df_block, item_and_grouping.for_all) for df_block in filtered_blocks
    ]

    # TODO Cleanup df_inventory + mp_to_df_block:
    #      df_inventory should also return a list of DfBlocks or similar.
    mp_to_df_block = {df_block.mountpoint: df_block for df_block in df_blocks}

    for mountpoint, params in df_discovery(host_extra_conf(host_name(), filesystem_groups), mplist):
        df_block = mp_to_df_block.get(mountpoint)
        if "patterns" in params:
            # Add the grouping_behaviour info to the discovered parameters of this service. With this information
            # the check can easily reconstruct the discovered grouping.
            params["grouping_behaviour"] = item_and_grouping.grouping

        elif df_block:
            # Somehow the user wanted to see the volume name in the service description,
            # but the grouping itself is based on the mountpoint only
            # => The df_inventory returns a list of mountpoints and mountpoint groups
            # Add the volume name as prefix for single mountpoints
            mountpoint = _prepare_item_name(df_block, item_and_grouping.for_single)

        # We have to add these parameters in any case in order to reconstruct df blocks
        # in the check because "mountpoint" may contain a free-text group name.
        params["mountpoint_for_block_devices"] = mountpoint_for_block_devices.name
        params["item_appearance"] = item_and_grouping.item
        yield mountpoint, params


# Legacy params
def _get_mountpoint_from_item(item, params, df_blocks):
    item_to_mp = {
        _prepare_item_name(df_block, ItemBehaviour.volume_name): df_block.mountpoint
        for df_block in df_blocks
    }
    item_to_mp.update(
        {
            _prepare_item_name(df_block, ItemBehaviour.uuid): df_block.mountpoint
            for df_block in df_blocks
        }
    )

    if "patterns" in params or item in [df_block.mountpoint for df_block in df_blocks]:
        return item

    if item in item_to_mp:
        return item_to_mp[item]

    return item


def check_df(item, params, parsed):
    mountpoint_for_block_devices = _get_item_behaviour_for_block_devices(params)
    item_and_grouping = _get_item_and_grouping(params)

    df_blocks, df_inodes = parsed
    df_blocks = _handle_block_devices(df_blocks, mountpoint_for_block_devices)

    item = _get_mountpoint_from_item(item, params, df_blocks)

    raw_df_blocks = [
        (
            _prepare_item_name(df_block, item_and_grouping.for_all),
            df_block.size_mb,
            df_block.avail_mb,
            df_block.reserved_mb,
        )
        for df_block in df_blocks
    ]
    raw_df_inodes = [
        (_prepare_item_name(df_inode, item_and_grouping.for_all), df_inode.total, df_inode.avail)
        for df_inode in df_inodes
    ]

    if params.get("show_volume_name"):
        volume_name = [d.device for d in df_blocks if d.mountpoint == item][0]
        yield 0, f"[{volume_name}]"

    yield from df_check_filesystem_list_coroutine(item, params, raw_df_blocks, raw_df_inodes)


check_info["df"] = {
    "inventory_function": inventory_df,
    "check_function": check_df,
    "service_description": "Filesystem %s",
    "has_perfdata": True,
    "group": "filesystem",
    "default_levels_variable": "filesystem_default_levels",
}
