#!/usr/bin/env python3
"""
Checkmk Special Agent for Icinga
"""
import argparse
import re
import sys
import xml.etree.ElementTree as ET
from typing import List, Dict, Any

import requests
from requests.auth import HTTPBasicAuth

from urllib3.exceptions import InsecureRequestWarning
from urllib3 import disable_warnings


parser = argparse.ArgumentParser(description="Icinga Special Agent")
parser.add_argument("--hostname")
parser.add_argument("--username")
parser.add_argument("--password")
parser.add_argument("--no-verify", action="store_true", default=False, help="Disable SSL certificate verification")
parser.add_argument("--timeout", type=int, default=30, help="Request timeout in seconds")
parser.add_argument("--no-group", action="store_true", default=False,
                    help="Emit one service per sub item instead of grouping them into the parent service")
parser.add_argument("--piggyback-prefix", default="",
                    help="Prefix prepended to every piggyback host name")

args = parser.parse_args()

hostname = args.hostname
username = args.username
password = args.password
ssl_verify = not args.no_verify
timeout = args.timeout
group_services = not args.no_group
piggyback_prefix = args.piggyback_prefix

if not ssl_verify:
    disable_warnings(InsecureRequestWarning)


auth = HTTPBasicAuth(username, password)


def sanitize_metric_name(label: str, check_name: str, used: set) -> str:
    """
    Turn an Icinga perfdata label into a valid Checkmk metric name.

    Checkmk only accepts ``[a-z0-9_]`` and the name must not start with a
    digit. Invalid characters (``/``, ``.``, ``-``, spaces, ...) are replaced
    by underscores. If nothing usable is left, or the name would start with a
    digit, it is derived from the check name. Uniqueness within one service is
    ensured by appending a counter.
    """
    name = re.sub(r"[^a-z0-9_]", "_", (label or "").lower())
    name = re.sub(r"_+", "_", name).strip("_")

    check = re.sub(r"[^a-z0-9_]", "_", (check_name or "").lower())
    check = re.sub(r"_+", "_", check).strip("_")

    if not name:
        name = check or "metric"
    elif name[0].isdigit():
        name = f"{check}_{name}" if check else f"m_{name}"

    candidate = name
    counter = 2
    while candidate in used:
        candidate = f"{name}_{counter}"
        counter += 1
    used.add(candidate)
    return candidate


def convert_perfdata(perf_list: List[Dict[str, Any]], check_name: str) -> str:
    """
    Convert Icinga performance data into Checkmk local check format, with
    sanitized metric names and empty fields for missing thresholds.
    """
    if not perf_list:
        return "-"
    result = []
    used: set = set()
    for entry in perf_list:
        if isinstance(entry, dict):
            name = sanitize_metric_name(entry.get("label", ""), check_name, used)
            fields = [entry.get("warn"), entry.get("crit"), entry.get("min"), entry.get("max")]
            tail = ";".join("" if f is None else str(f) for f in fields)
            result.append(f"{name}={entry.get('value')};{tail}")
        else:
            # Pre-formatted string: sanitize the metric name part if present.
            text = str(entry)
            if "=" in text:
                label, _, rest = text.partition("=")
                text = f"{sanitize_metric_name(label, check_name, used)}={rest}"
            result.append(text)
    return "|".join(result)


# Map an Icinga/Nagios status text to a Checkmk state code.
_STATE_BY_TEXT = {
    "ok": 0, "up": 0,
    "warning": 1, "warn": 1,
    "critical": 2, "crit": 2, "down": 2,
    "unknown": 3,
}

# Severity ranking used to pick the worst state. Checkmk treats CRIT as worse
# than UNKNOWN (OK < WARN < UNKNOWN < CRIT), so a plain max() over the state
# codes would be wrong (it would rank UNKNOWN=3 above CRIT=2).
_SEVERITY = {0: 0, 1: 1, 3: 2, 2: 3}

# Cosmetic status icons for the grouped service output. Plain Unicode emoji
# (UTF-8) — they render in the Checkmk GUI; the actual service state still
# comes from the worst sub-state, not from these.
_STATE_ICONS = {0: "🟢", 1: "🟡", 2: "🔴", 3: "⚪"}


def _parse_state(raw_state):
    """Full state derivation: map a status cell to OK/WARN/CRIT/UNKNOWN.

    An empty cell counts as OK; any text we do not recognise becomes UNKNOWN
    rather than silently CRIT.
    """
    if not raw_state:
        return 0
    return _STATE_BY_TEXT.get(raw_state.strip().lower(), 3)


def _worst(*states):
    """Return the worst state by Checkmk severity, not by numeric value."""
    return max(states, key=lambda s: _SEVERITY.get(s, _SEVERITY[3]))


def convert_output(raw_output):
    if "<table>" not in raw_output:
        return raw_output.replace("\n", ", ")

    try:
        root = ET.fromstring(f"<root>{raw_output}</root>")
    except ET.ParseError:
        return raw_output.replace("\n", ", ")

    table = root.find("table")
    rows = list(table)
    headers = [cell.text for cell in rows[0]]

    data = []
    for row in rows[1:]:
        line = []
        cells = [cell.text for cell in row]
        # fehlende Status-Spalte auffüllen
        state = 0
        while len(cells) < len(headers):
            cells.append(None)
        for idx, header in enumerate(headers):
            if header.lower() in ["status", "state"]:
                raw_state = cells[idx]
                state = _parse_state(raw_state)
                if not raw_state:
                    continue
            line.append(f"{header}: {cells[idx]}")
        data.append((state, line))

    return data


API_ENDPOINT_SERVICES = "/v1/objects/services"


def main():
    """Main function to structure the code properly"""

    # Get All Services
    url = f"https://{hostname}{API_ENDPOINT_SERVICES}"
    try:
        response = requests.get(url, auth=auth, timeout=timeout, verify=ssl_verify)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f"Error connecting to Icinga: {e}", file=sys.stderr)
        sys.exit(1)

    for result_set in response.json()["results"]:
        attrs = result_set.get("attrs")
        if not attrs:
            continue
        print(f"<<<<{piggyback_prefix}{attrs['host_name']}>>>>")
        print("<<<local>>>")

        service_name = attrs["name"]
        state = int(attrs["state"])
        output = convert_output(attrs["last_check_result"]["output"])
        perfdata = convert_perfdata(attrs["last_check_result"]["performance_data"], service_name)

        if isinstance(output, list):
            detail_lines = []
            worst = state
            for sub_state, sub_data in output:
                sub_name = sub_data[0] if sub_data else ""
                sub_output = sub_data[1] if len(sub_data) > 1 else ""
                sub_name = sub_name.replace("Parameter: ", "")  # hack
                if group_services:
                    icon = _STATE_ICONS.get(sub_state, _STATE_ICONS[3])
                    detail_lines.append(f"{icon} " + ", ".join(c for c in sub_data if c))
                    worst = _worst(worst, sub_state)
                else:
                    print(f'{sub_state} "{service_name} {sub_name}" - {sub_output}')

            if group_services:
                # One grouped service: rows folded into the details (\n), each
                # prefixed with its status icon. The summary line stays plain —
                # icons live in the details only.
                summary = f"General Service {service_name}"
                text = summary
                if detail_lines:
                    text = summary + "\\n" + "\\n".join(detail_lines)
                print(f'{worst} "{service_name}" {perfdata} {text}')
                continue
            output = f"General Service {service_name}"

        print(f'{state} "{service_name}" {perfdata} {output}')

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


if __name__ == "__main__":
    main()
