#!/usr/bin/env python3
# -*- encoding: utf-8; py-indent-offset: 4 -*-

# Maintained by MARIS Healthcare GmbH <https://www.maris-healthcare.de>
# Based on original Checkmk plugin work by Kleinrotti <https://github.com/Kleinrotti/checkmk_mirth>.

import argparse
import json
import os

from datetime import datetime
from pathlib import Path

import requests
import urllib3


urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


# ----------------------------------------------------------------------
# Configuration / globals
# ----------------------------------------------------------------------

bridgelink_url = ""
username = ""
secret = ""
verify_ssl = True
debug = False
log_fetch_size = 10
log_services = True
tmp_dir = None


# ----------------------------------------------------------------------
# BridgeLink log state mapping
# ----------------------------------------------------------------------

bridgelink_log_states = {
    "INFO": "I",
    "ERROR": "C",
    "WARNING": "W",
    "Disconnected": "C",
    "Idle": "I",
    "Connected": "I",
    "Connecting": "I",
    "Receiving": "I",
    "Info": "I",
    "Waiting for Response": "W",
    "Sending": "I",
    "Polling": "I",
    "Writing": "I",
}


# ----------------------------------------------------------------------
# HTTP requests
# ----------------------------------------------------------------------

def request(
        api_url_suffix: str,
        headers=None,
) -> str:

    if headers is None:
        headers = {
            "X-Requested-With": "checkmk_bridgelink",
            "Accept": "application/json",
        }

    url = f"{bridgelink_url}{api_url_suffix}"

    if debug:
        print(
            f"DEBUG: GET {url}",
            file=os.sys.stderr,
        )

    try:
        response = requests.get(
            url,
            headers=headers,
            auth=(username, secret),
            verify=verify_ssl,
            timeout=30,
        )

    except requests.exceptions.RequestException as exc:
        raise RuntimeError(
            f"Unable to connect to BridgeLink API: {exc}"
        ) from exc

    if debug:
        print(
            f"DEBUG: HTTP {response.status_code}",
            file=os.sys.stderr,
        )
        print(
            f"DEBUG: {response.text}",
            file=os.sys.stderr,
        )

    if response.status_code != 200:
        raise RuntimeError(
            f"BridgeLink API returned HTTP "
            f"{response.status_code}: {response.text}"
        )

    return response.text


# ----------------------------------------------------------------------
# API calls
# ----------------------------------------------------------------------

def request_channels():
    response = request(
        "/channels/statuses?includeUndeployed=true"
    )

    data = json.loads(response)

    return data.get("list")


def request_channel_statistics(channel_id: str):
    response = request(
        f"/channels/{channel_id}/statistics"
    )

    data = json.loads(response)

    return data.get("channelStatistics", {})


def request_channel_connection_state(
        channel_id: str,
):
    headers = {
        "X-Requested-With": "checkmk_bridgelink",
        "Accept": "text/plain",
    }

    return request(
        f"/extensions/dashboardstatus/"
        f"channelStates/{channel_id}",
        headers,
    ).strip()


def request_server_log():
    response = request(
        f"/extensions/serverlog"
        f"?fetchSize={log_fetch_size}"
    )

    return json.loads(response).get("list")


def request_connection_log():
    response = request(
        f"/extensions/dashboardstatus/"
        f"connectionLogs?fetchSize={log_fetch_size}"
    )

    return json.loads(response).get("linked-list")


def request_server_id():
    headers = {
        "X-Requested-With": "checkmk_bridgelink",
        "Accept": "text/plain",
    }

    return request(
        "/server/id",
        headers,
    ).strip()


def request_subscription():
    # BridgeLink exposes subscription state via the historical licenseInfo API.
    response = request(
        "/server/licenseInfo"
    )

    data = json.loads(response)

    return data.get(
        "com.mirth.connect.model.LicenseInfo",
        {},
    )


def request_version():
    headers = {
        "X-Requested-With": "checkmk_bridgelink",
        "Accept": "text/plain",
    }

    return request(
        "/server/version",
        headers,
    ).strip()


# ----------------------------------------------------------------------
# Checkmk BridgeLink server section
# ----------------------------------------------------------------------

def process_server_info():

    subscription_info = request_subscription()

    server_id = request_server_id()
    version = request_version()

    subscription_activated = subscription_info.get(
        "activated",
        False,
    )

    subscription_online = subscription_info.get(
        "online",
        False,
    )

    expiration_date = subscription_info.get(
        "expirationDate",
        "-",
    )

    print(
        "<<<bridgelink_server:sep(124)>>>"
    )

    print(
        "|".join(
            [
                str(server_id),
                str(subscription_activated),
                str(subscription_online),
                str(expiration_date),
                str(version),
            ]
        )
    )


# ----------------------------------------------------------------------
# Metric handling
# ----------------------------------------------------------------------

def calculate_metrics(
        channel_id: str,
        statistics: dict,
) -> dict:

    current_metrics = {
        "sent": int(
            statistics.get("sent", 0)
        ),
        "received": int(
            statistics.get("received", 0)
        ),
        "filtered": int(
            statistics.get("filtered", 0)
        ),
        "error": int(
            statistics.get("error", 0)
        ),
        "queued": int(
            statistics.get("queued", 0)
        ),
    }

    state_file = tmp_dir / (
        f"last_metric_{channel_id}.json"
    )

    #
    # First execution
    #
    # BridgeLink returns cumulative counters.
    # On the first run we do not want to produce
    # a huge spike in Checkmk.
    #

    if not state_file.exists():

        state_file.write_text(
            json.dumps(current_metrics),
            encoding="utf-8",
        )

        return {
            "sent": 0,
            "received": 0,
            "filtered": 0,
            "error": 0,
            "queued": 0,
        }

    try:
        previous_metrics = json.loads(
            state_file.read_text(
                encoding="utf-8"
            )
        )

    except (
            json.JSONDecodeError,
            OSError,
    ):
        previous_metrics = {}

    #
    # Always save current cumulative values
    #

    state_file.write_text(
        json.dumps(current_metrics),
        encoding="utf-8",
    )

    result = {}

    for metric_name, current_value in (
            current_metrics.items()
    ):

        previous_value = int(
            previous_metrics.get(
                metric_name,
                current_value,
            )
        )

        #
        # Counter increased
        #

        if current_value > previous_value:
            result[metric_name] = (
                    current_value
                    - previous_value
            )

        #
        # Same value or counter reset
        #

        else:
            result[metric_name] = 0

    return result


# ----------------------------------------------------------------------
# Checkmk BridgeLink channel section
# ----------------------------------------------------------------------

def process_channels(channel_json):

    print(
        "<<<bridgelink_channel:sep(124)>>>"
    )

    if channel_json is None:
        return

    dashboard_status = channel_json.get(
        "dashboardStatus"
    )

    if dashboard_status is None:
        return

    #
    # BridgeLink/Mirth API returns a single object
    # instead of a list when only one channel exists.
    #

    if isinstance(
            dashboard_status,
            list,
    ):
        channels = dashboard_status
    else:
        channels = [
            dashboard_status
        ]

    for channel_data in channels:

        channel_id = str(
            channel_data.get(
                "channelId",
                "",
            )
        )

        channel_name = str(
            channel_data.get(
                "name",
                "",
            )
        )

        channel_state = str(
            channel_data.get(
                "state",
                "UNKNOWN",
            )
        )

        #
        # Connection state
        #

        if (
                channel_state.lower()
                == "undeployed"
        ):
            connection_state = (
                channel_state
            )

        else:
            try:
                connection_state = (
                    request_channel_connection_state(
                        channel_id
                    )
                )

            except Exception:
                connection_state = (
                    "Unknown"
                )

        #
        # Statistics
        #

        statistics = (
            request_channel_statistics(
                channel_id
            )
        )

        metrics = calculate_metrics(
            channel_id,
            statistics,
        )

        #
        # IMPORTANT:
        #
        # Order must match bridgelink_channel.py:
        #
        # name
        # id
        # state
        # channelState
        # sent
        # received
        # filtered
        # error
        # queued
        #

        print(
            "|".join(
                [
                    channel_name,
                    channel_id,
                    channel_state,
                    connection_state,
                    str(
                        metrics["sent"]
                    ),
                    str(
                        metrics[
                            "received"
                        ]
                    ),
                    str(
                        metrics[
                            "filtered"
                        ]
                    ),
                    str(
                        metrics["error"]
                    ),
                    str(
                        metrics["queued"]
                    ),
                ]
            )
        )


# ----------------------------------------------------------------------
# Server log
# ----------------------------------------------------------------------

def process_server_log(logs):

    print(
        "<<<logwatch>>>"
    )

    print(
        "[[[BridgeLink Server Events]]]"
    )

    if logs is None:
        return

    entries = logs.get(
        "com.mirth.connect.plugins."
        "serverlog.ServerLogItem"
    )

    if entries is None:
        return

    current_date = datetime.utcnow()

    for log in entries:

        try:
            log_date = datetime.strptime(
                log["date"],
                "%Y-%m-%d %H:%M:%S.%f %Z",
            )

        except (
                KeyError,
                ValueError,
        ):
            continue

        delta_minutes = (
                                current_date
                                - log_date
                        ).total_seconds() / 60

        safe_timestamp = (
            log["date"]
            .replace("/", "_")
        )

        state_file = (
                tmp_dir
                / (
                        "server_event_"
                        + safe_timestamp
                )
        )

        #
        # Only events from the last 10 minutes
        #

        if delta_minutes < 10:

            if not state_file.exists():

                level = str(
                    log.get(
                        "level",
                        "INFO",
                    )
                )

                checkmk_state = (
                    bridgelink_log_states.get(
                        level,
                        "I",
                    )
                )

                message = str(
                    log.get(
                        "message",
                        ""
                    )
                )

                print(
                    f"{checkmk_state} "
                    f"{message}"
                )

                state_file.touch()

        else:
            state_file.unlink(
                missing_ok=True
            )


# ----------------------------------------------------------------------
# Connection log
# ----------------------------------------------------------------------

def process_connection_log(logs):

    print(
        "<<<logwatch>>>"
    )

    print(
        "[[[BridgeLink Connection Events]]]"
    )

    if logs is None:
        return

    entries = logs.get(
        "com.mirth.connect.plugins."
        "dashboardstatus.ConnectionLogItem"
    )

    if entries is None:
        return

    current_date = datetime.now()

    for log in entries:

        try:
            log_date = datetime.strptime(
                log["dateAdded"],
                "%Y-%m-%d %H:%M:%S.%f",
            )

        except (
                KeyError,
                ValueError,
        ):
            continue

        delta_minutes = (
                                current_date
                                - log_date
                        ).total_seconds() / 60

        safe_timestamp = (
            log["dateAdded"]
            .replace("/", "_")
        )

        state_file = (
                tmp_dir
                / (
                        "connection_event_"
                        + safe_timestamp
                )
        )

        if delta_minutes < 10:

            if not state_file.exists():

                event_state = str(
                    log.get(
                        "eventState",
                        "Info",
                    )
                )

                connector_type = str(
                    log.get(
                        "connectorType",
                        "Unknown",
                    )
                )

                checkmk_state = (
                    bridgelink_log_states.get(
                        event_state,
                        "I",
                    )
                )

                print(
                    f"{checkmk_state} "
                    f"{connector_type} -> "
                    f"{event_state}"
                )

                state_file.touch()

        else:
            state_file.unlink(
                missing_ok=True
            )


# ----------------------------------------------------------------------
# Cleanup
# ----------------------------------------------------------------------

def cleanup_event_files():

    if not tmp_dir.exists():
        return

    current_timestamp = (
        datetime.now().timestamp()
    )

    for state_file in (
            tmp_dir.iterdir()
    ):

        if not state_file.is_file():
            continue

        if not (
                state_file.name.startswith(
                    "connection_event_"
                )
                or
                state_file.name.startswith(
                    "server_event_"
                )
        ):
            continue

        try:
            age_minutes = (
                                  current_timestamp
                                  - state_file.stat().st_mtime
                          ) / 60

        except OSError:
            continue

        if age_minutes > 60:
            state_file.unlink(
                missing_ok=True
            )


# ----------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------

def main(args):

    global bridgelink_url
    global username
    global secret
    global verify_ssl
    global debug
    global log_fetch_size
    global log_services
    global tmp_dir

    username = args.username
    secret = args.secret

    verify_ssl = args.ssl
    debug = args.debug

    log_fetch_size = (
        args.log
    )

    log_services = (
        args.log_services
    )

    #
    # Checkmk site local temporary directory
    #

    tmp_dir = Path(
        os.getenv(
            "OMD_ROOT",
            "/",
        ),
        "tmp/check_mk/"
        "special_agent_bridgelink",
    )

    tmp_dir.mkdir(
        parents=True,
        exist_ok=True,
    )

    #
    # BridgeLink REST API
    #

    bridgelink_url = (
        f"https://"
        f"{args.ip}:"
        f"{args.port}"
        f"/api"
    )

    #
    # Channels
    #

    channels = request_channels()

    process_channels(
        channels
    )

    #
    # Server
    #

    process_server_info()

    #
    # Logs
    #

    if log_services:

        process_server_log(
            request_server_log()
        )

        process_connection_log(
            request_connection_log()
        )

        cleanup_event_files()


# ----------------------------------------------------------------------
# Command line
# ----------------------------------------------------------------------

if __name__ == "__main__":

    parser = argparse.ArgumentParser(
        description=(
            "Checkmk Special Agent "
            "for BridgeLink"
        )
    )

    parser.add_argument(
        "-i",
        "--ip",
        required=True,
        help=(
            "IP address or FQDN of "
            "the BridgeLink API endpoint"
        ),
    )

    parser.add_argument(
        "-u",
        "--username",
        required=True,
        help=(
            "Username for the "
            "BridgeLink API"
        ),
    )

    parser.add_argument(
        "-s",
        "--secret",
        required=True,
        help=(
            "Password for the "
            "BridgeLink API"
        ),
    )

    parser.add_argument(
        "-p",
        "--port",
        default="8443",
        help="BridgeLink HTTPS port",
    )

    parser.add_argument(
        "-v",
        "--ssl",
        action="store_true",
        default=False,
        help=(
            "Verify the BridgeLink "
            "SSL certificate"
        ),
    )

    parser.add_argument(
        "-l",
        "--log",
        type=int,
        default=10,
        help=(
            "Maximum number of log "
            "entries to fetch"
        ),
    )

    parser.add_argument(
        "-ls",
        "--log-services",
        dest="log_services",
        action="store_true",
        default=False,
        help=(
            "Enable BridgeLink "
            "Logwatch services"
        ),
    )

    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="Enable debug output",
    )

    arguments = parser.parse_args()

    main(arguments)
