#!/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.

# <<<mssql_backup>>>
# MSSQL_SQLEXPRESS1 test123 1331207325

# <<<mssql_backup>>>
# MSSQL_SQL0x2 master 2016-07-08 20:20:27
# MSSQL_SQL0x2 model 2016-07-08 20:20:28
# MSSQL_SQL0x2 model 2016-07-12 09:09:42
# MSSQL_SQL0x2 model 2016-07-11 20:20:07
# MSSQL_SQL0x2 msdb 2016-07-08 20:20:43
# MSSQL_SQL0x2 msdb 2016-07-11 20:20:07

# <<<mssql_backup>>>
# MSSQL_SQL0x3 master 2016-07-08 20:20:27 D
# MSSQL_SQL0x3 model 2016-07-08 20:20:28 D
# MSSQL_SQL0x3 model 2016-07-12 09:09:42 L
# MSSQL_SQL0x3 model 2016-07-11 20:20:07 I
# MSSQL_SQL0x3 msdb 2016-07-08 20:20:43 D
# MSSQL_SQL0x3 msdb 2016-07-11 20:20:07 I

# <<<mssql_backup:sep(124)>>>
# MSSQL_SQL0x4|master|2016-07-08 20:20:27|D
# MSSQL_SQL0x4|model|2016-07-08 20:20:28|D
# MSSQL_SQL0x4|model|2016-07-12 09:09:42|L
# MSSQL_SQL0x4|model|2016-07-11 20:20:07|I
# MSSQL_SQL0x4|msdb|2016-07-08 20:20:43|D
# MSSQL_SQL0x4|msdb|2016-07-11 20:20:07|I

discovery_mssql_backup = []

factory_settings["mssql_backup_default_levels"] = {
    "database": (None, None),
    "database_diff": (None, None),
    "log": (None, None),
    "file_or_filegroup": (None, None),
    "file_diff": (None, None),
    "partial": (None, None),
    "partial_diff": (None, None),
    "unspecific": (None, None),
}


def parse_mssql_backup(info):
    def _parse_date_and_time(b_date, b_time):
        try:
            if b_time is None:
                return int(b_date)
            return time.mktime(time.strptime("%s %s" % (b_date, b_time), "%Y-%m-%d %H:%M:%S"))
        except ValueError:
            return None

    map_backup_types = {
        "D": "database",
        "I": "database diff",
        "L": "log",
        "F": "file or filegroup",
        "G": "file diff",
        "P": "partial",
        "Q": "partial diff",
        "-": "unspecific",
    }

    parsed = {}
    Backup = collections.namedtuple(  # pylint: disable=collections-namedtuple-call
        "Backup", ["timestamp", "type", "state"]
    )
    for line in info:
        if len(line) <= 2:
            continue
        # handle one special case where spaces are in date/time:
        if len(line) == 4 and " " in line[2]:
            line = line[:2] + line[2].split(" ") + line[3:]

        # (fill up with Nones)
        inst, tablespace, b_date, b_time, b_type, b_state = line + [None] * (6 - len(line))
        timestamp = _parse_date_and_time(b_date, b_time)

        item = "%s %s" % (inst, tablespace)
        backup = Backup(timestamp, map_backup_types.get(b_type), b_state or "")
        parsed.setdefault(item, []).append(backup)

    return parsed


def inventory_mssql_backup(parsed):
    discovery_mode = _mssql_backup_discovery_mode()
    if discovery_mode != "summary":
        return
    for db_name in parsed:
        yield db_name, {}


def _mssql_backup_discovery_mode():
    rules = host_extra_conf(host_name(), discovery_mssql_backup)
    try:
        return rules[0]["mode"]
    except (IndexError, KeyError):
        return "summary"  # default, comp. to older versions


def check_mssql_backup(item, params, parsed):
    data = parsed.get(item)
    if data is None:
        # Assume general connection problem to the database, which is reported
        # by the "X Instance" service and skip this check.
        raise MKCounterWrapped("Failed to connect to database")

    if not isinstance(params, dict):
        params = {"database": params}

    for backup in data:
        if backup.state == "no backup found":
            yield params.get("not_found", 1), "No backup found"
            continue
        if backup.state.startswith("ERROR: "):
            yield 2, backup.state[7:]
            continue
        if backup.type is None:
            backup_type_var = "database"
            perfkey = "seconds"
            backup_type_info = "[database]"
        else:
            backup_type_var = backup.type.strip().replace(" ", "_")
            perfkey = "backup_age_%s" % backup_type_var
            backup_type_info = "[%s]" % backup.type
        state, infotext, perfdata = _check_mssql_backup(
            backup, params.get(backup_type_var, (None, None)), perfkey
        )
        yield state, "%s %s" % (backup_type_info, infotext), perfdata


def _check_mssql_backup(backup, levels, perfkey):
    state = 0
    age_warn, age_crit = levels
    sec_ago = time.time() - backup.timestamp
    if age_crit is not None and sec_ago >= age_crit:
        state = 2
    elif age_warn is not None and sec_ago >= age_warn:
        state = 1

    infotext = "Last backup was at %s (%s ago)" % (
        time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(backup.timestamp)),
        get_age_human_readable(sec_ago),
    )

    if state:
        infotext += " (warn/crit at %s/%s)" % (
            get_age_human_readable(age_warn),
            get_age_human_readable(age_crit),
        )

    return state, infotext, [(perfkey, sec_ago, age_warn, age_crit)]


check_info["mssql_backup"] = {
    "parse_function": parse_mssql_backup,
    "check_function": check_mssql_backup,
    "inventory_function": inventory_mssql_backup,
    "service_description": "MSSQL %s Backup",
    "has_perfdata": True,
    "group": "mssql_backup",
    "default_levels_variable": "mssql_backup_default_levels",
}

# .
#   .--single--------------------------------------------------------------.
#   |                          _             _                             |
#   |                      ___(_)_ __   __ _| | ___                        |
#   |                     / __| | '_ \ / _` | |/ _ \                       |
#   |                     \__ \ | | | | (_| | |  __/                       |
#   |                     |___/_|_| |_|\__, |_|\___|                       |
#   |                                  |___/                               |
#   '----------------------------------------------------------------------'


def _mssql_backup_per_type_item(db_name, backup):
    if backup.type is None:
        return "%s UNKNOWN" % db_name
    return "%s %s" % (db_name, backup.type.title())


def inventory_mssql_backup_per_type(parsed):
    discovery_mode = _mssql_backup_discovery_mode()
    if discovery_mode != "per_type":
        return
    for db_name, attrs in parsed.items():
        for backup in attrs:
            yield _mssql_backup_per_type_item(db_name, backup), {}


def check_mssql_backup_per_type(item, params, parsed):
    for db_name, attrs in parsed.items():
        for backup in attrs:
            if item == _mssql_backup_per_type_item(db_name, backup):
                return _check_mssql_backup(backup, params.get("levels", (None, None)), "backup_age")
    # Assume general connection problem to the database, which is reported
    # by the "X Instance" service and skip this check.
    raise MKCounterWrapped("Failed to connect to database")


check_info["mssql_backup.per_type"] = {
    "check_function": check_mssql_backup_per_type,
    "inventory_function": inventory_mssql_backup_per_type,
    "service_description": "MSSQL %s Backup",
    "has_perfdata": True,
    "group": "mssql_backup_per_type",
}
