#!/usr/bin/env python3
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# Freshservice ITSM
# +------------------------------------------------------------------+
#      _____ ____   _____    _____  ____  _               _____  
#     |_   _|  _ \ / ____|  / ____|/ __ \| |        /\   |  __ \ 
#       | | | |_) | |      | (___ | |  | | |       /  \  | |__) |
#       | | |  _ <| |       \___ \| |  | | |      / /\ \ |  _  / 
#      _| |_| |_) | |____   ____) | |__| | |____ / ____ \| | \ \ 
#     |_____|____/ \_____| |_____/ \____/|______/_/    \_\_|  \_\
#                                                                                                                        
# +------------------------------------------------------------------+
# Copyright  IBC SOLAR AG - License: GNU General Public License v2.
# Author: Marvin Sobeck
# Link: https://www.ibc-solar.com/
# Written for the Freshservice ITSM for automatic ticket creation

import json
import os
import sys
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urljoin

import requests


SUCCESS_CODES = {200, 201, 202, 204}


# ------------------------- environment helpers -------------------------

def _env(name: str, default: str = "") -> str:
    return os.environ.get(name, default)


def _get_first(*names: str, default: Optional[str] = None) -> Optional[str]:
    for n in names:
        v = os.environ.get(n)
        if v not in (None, ""):
            return v
    return default


# ------------------------- chunked password reconstruction -------------------------

def _parse_suffix_tuple(sfx: str) -> Tuple[int, ...]:
    if not sfx:
        return ()
    out: List[int] = []
    for tok in sfx.split("_"):
        if tok.isdigit():
            out.append(int(tok))
        else:
            num = "".join(ch for ch in tok if ch.isdigit())
            if num:
                out.append(int(num))
    return tuple(out)


def _collect_chunks(prefix: str) -> List[Tuple[str, Tuple[int, ...], str]]:
    out: List[Tuple[str, Tuple[int, ...], str]] = []
    plen = len(prefix)
    for k, v in os.environ.items():
        if not k.startswith(prefix):
            continue
        sfx = k[plen:]
        if sfx.startswith("_"):
            sfx = sfx[1:]
        out.append((k, _parse_suffix_tuple(sfx), v))
    return out


def _candidate_values_from_chunks(prefix: str) -> List[str]:
    chunks = _collect_chunks(prefix)
    if not chunks:
        return []
    parts_num = sorted(chunks, key=lambda t: t[1])
    cands = ["".join(v for _, _, v in parts_num if v)]
    for _, _, v in parts_num:
        if v:
            cands.append(v)

    seen = set()
    uniq: List[str] = []
    for s in cands:
        if s and s not in seen:
            seen.add(s)
            uniq.append(s)
    return uniq


def _get_param_value_upper(name_upper: str) -> str:
    direct = os.environ.get(f"NOTIFY_PARAMETER_{name_upper}")
    if direct not in (None, ""):
        return direct
    cands = _candidate_values_from_chunks(f"NOTIFY_PARAMETER_{name_upper}")
    return max(cands, key=len) if cands else ""


def _retrieve_from_passwordstore(store_key: str) -> str:
    try:
        from cmk.notification_plugins import utils
        return (utils.retrieve_from_passwordstore(f"store {store_key}") or "").strip()
    except Exception as e:
        raise RuntimeError(f"Could not retrieve password from password store: {e}") from e


def _get_secret_param(name_upper: str) -> str:
    # String fall
    direct = (os.environ.get(f"NOTIFY_PARAMETER_{name_upper}") or "").strip()
    if direct:
        return direct

    # Retrieve password from Ruleset API v1
    marker = (os.environ.get(f"NOTIFY_PARAMETER_{name_upper}_1") or "").strip()
    mode = (os.environ.get(f"NOTIFY_PARAMETER_{name_upper}_2") or "").strip()

    if marker == "cmk_postprocessed":
        # direct secret
        if mode == "explicit_password":
            return (os.environ.get(f"NOTIFY_PARAMETER_{name_upper}_3_2") or "").strip()

        # password store
        if mode == "stored_password":
            store_key = (os.environ.get(f"NOTIFY_PARAMETER_{name_upper}_3_1") or "").strip()
            if not store_key:
                return ""
            return _retrieve_from_passwordstore(store_key)

    # Fallback for old plugin variants
    return _get_param_value_upper(name_upper).strip()


# ------------------------- Checkmk context helpers -------------------------

def _normalize_newlines(value: str) -> str:
    return (value or "").replace("\\n", "\n").strip()


def _summary() -> str:
    if _is_service():
        host = _hostname()
        service = _env("NOTIFY_SERVICEDESC")
        output = _normalize_newlines(_env("NOTIFY_SERVICEOUTPUT"))
        return f"{host} / {service}: {output}"

    host = _hostname()
    output = _normalize_newlines(_env("NOTIFY_HOSTOUTPUT"))
    return f"{host} / {output}"


def _details() -> str:
    if _is_service():
        return _normalize_newlines(_env("NOTIFY_LONGSERVICEOUTPUT"))
    return _normalize_newlines(_env("NOTIFY_LONGHOSTOUTPUT"))


def _source_state() -> str:
    if _is_service():
        return _env("NOTIFY_SERVICESTATE")
    return _env("NOTIFY_HOSTSTATE")


def _previous_state() -> str:
    if _is_service():
        return (
            _env("NOTIFY_PREVIOUSSERVICEHARDSTATE")
            or _env("NOTIFY_LASTSERVICESTATE")
            or ""
        )
    return (
        _env("NOTIFY_PREVIOUSHOSTHARDSTATE")
        or _env("NOTIFY_LASTHOSTSTATE")
        or ""
    )


def _state_change() -> str:
    previous = _previous_state()
    current = _source_state()
    if previous:
        return f"{previous} -> {current}"
    return current


def _site() -> str:
    return _env("NOTIFY_OMD_SITE") or _env("OMD_SITE")


def _event_date() -> str:
    return _env("NOTIFY_LONGDATETIME") or _env("NOTIFY_SHORTDATETIME")


def _is_service() -> bool:
    return _env("NOTIFY_WHAT") == "SERVICE"


def _compact(d: Dict[str, Any]) -> Dict[str, Any]:
    return {k: v for k, v in d.items() if v not in ("", None)}


def _absolute_url(base_url: str, relative_url: str) -> str:
    if not relative_url:
        return ""
    if relative_url.startswith(("http://", "https://")):
        return relative_url
    if not base_url:
        return relative_url
    return urljoin(base_url.rstrip("/") + "/", relative_url.lstrip("/"))


def _hostname() -> str:
    return (_env("NOTIFY_HOSTNAME") or _env("NOTIFY_HOSTALIAS") or "").strip()


def _normalize_severity(raw: str) -> str:
    value = (raw or "").strip().upper()
    if value in ("UP", "OK"):
        return "ok"
    if value in ("WARN", "WARNING"):
        return "warning"
    if value in ("UNKNOWN", "UNREACH", "UNREACHABLE"):
        return "error"
    if value in ("CRIT", "CRITICAL", "DOWN"):
        return "critical"
    return "warning"


def _common_payload() -> Dict[str, Any]:
    return {
        "hostname": _hostname(),
        "ip_address": _env("NOTIFY_HOSTADDRESS"),
        "notification_comment": _env("NOTIFY_NOTIFICATIONCOMMENT"),
        "contact_name": _env("NOTIFY_CONTACTNAME"),
        "summary": _summary(),
        "service_details": _details(),
        "event_date": _event_date(),
        "site": _site(),
        "state_change": _state_change(),
    }


def _build_host_payload(checkmk_base_url: str) -> Dict[str, Any]:
    host_state = _env("NOTIFY_HOSTSTATE")
    payload = {
        **_common_payload(),
        "object_type": "host",
        "metric_name": "Availability",
        "subject": _summary(),
        "description": f"{_hostname()} / {_state_change()}",
        "severity": _normalize_severity(host_state),
        "source_state": host_state,
        "event_id": _env("NOTIFY_HOSTEVENTID"),
        "metric_value": _absolute_url(checkmk_base_url, _env("NOTIFY_HOSTURL")),
        "perf_data": _env("NOTIFY_HOSTPERFDATA"),
        "problem_id": _env("NOTIFY_HOSTPROBLEMID"),
    }
    return _compact(payload)


def _build_service_payload(checkmk_base_url: str) -> Dict[str, Any]:
    service_state = _env("NOTIFY_SERVICESTATE")
    service_name = _env("NOTIFY_SERVICEDESC")
    payload = {
        **_common_payload(),
        "object_type": "service",
        "service_name": service_name,
        "metric_name": service_name,
        "subject": _summary(),
        "description": f"{_hostname()} / {_env("NOTIFY_SERVICEDESC")}: {_state_change()}",
        "severity": _normalize_severity(service_state),
        "source_state": service_state,
        "event_id": _env("NOTIFY_SERVICEEVENTID"),
        "metric_value": _absolute_url(checkmk_base_url, _env("NOTIFY_SERVICEURL")),
        "perf_data": _env("NOTIFY_SERVICEPERFDATA"),
        "problem_id": _env("NOTIFY_SERVICEPROBLEMID"),
    }
    return _compact(payload)


# ------------------------- Freshservice HTTP -------------------------

def _headers(auth_key: str) -> Dict[str, str]:
    return {
        "Authorization": auth_key,
        "Content-Type": "application/json",
        "Accept": "application/json",
    }


def _send_alert(
    endpoint_url: str,
    auth_key: str,
    payload: Dict[str, Any],
    timeout: int,
) -> None:
    response = requests.post(
        endpoint_url,
        headers=_headers(auth_key),
        json=payload,
        timeout=timeout,
    )

    if response.status_code not in SUCCESS_CODES:
        raise RuntimeError(
            f"Freshservice POST failed: HTTP {response.status_code}: {response.text}"
        )


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

def main() -> int:
    endpoint_url = (_get_first("NOTIFY_PARAMETER_ENDPOINT_URL", default="") or "").strip()

    auth_key = _get_secret_param("AUTH_KEY")

    checkmk_base_url = _get_first("NOTIFY_PARAMETER_CHECKMK_BASE_URL", default="") or ""
    timeout_raw = _get_first("NOTIFY_PARAMETER_TIMEOUT_SECONDS", default="10") or "10"

    try:
        timeout = int(timeout_raw)
    except ValueError:
        timeout = 10

    if not endpoint_url:
        sys.stderr.write("Parameter endpoint_url missing\n")
        return 2

    if not auth_key:
        sys.stderr.write("Parameter auth_key missing\n")
        return 2

    payload = (
        _build_service_payload(checkmk_base_url)
        if _is_service()
        else _build_host_payload(checkmk_base_url)
    )

    try:
        _send_alert(
            endpoint_url=endpoint_url,
            auth_key=auth_key,
            payload=payload,
            timeout=timeout,
        )
    except Exception as e:
        sys.stderr.write(f"{type(e).__name__}: {e}\n")
        return 2

    return 0


if __name__ == "__main__":
    sys.exit(main())