#!/usr/bin/env python3
# cmk2ntfy
# Bulk: no

import os
import sys
import traceback
from typing import Any
from enum import IntEnum

# pylint: disable=import-error
from cmk.notification_plugins.utils import (
    host_url_from_context,
    process_by_status_code,
    service_url_from_context,
    substitute_context,
)

import requests
from cmk.utils.http_proxy_config import deserialize_http_proxy_config


def _normalize_context(context: Any) -> Any:
    """
    Ensure serialized tuples/strings from NOTIFY_ environment variables
    are properly normalized into Python structures for Checkmk helpers.
    """
    if isinstance(context, dict):
        url_prefix = context.get("PARAMETER_URL_PREFIX")
        if url_prefix and isinstance(url_prefix, str):
            val_trimmed = url_prefix.strip()
            if val_trimmed.startswith("(") or val_trimmed.startswith("["):
                import ast
                try:
                    parsed = ast.literal_eval(val_trimmed)
                    if isinstance(parsed, (tuple, list)):
                        context["PARAMETER_URL_PREFIX"] = tuple(parsed)
                except Exception:
                    pass
            elif val_trimmed.startswith("http://") or val_trimmed.startswith("https://"):
                context["PARAMETER_URL_PREFIX"] = ("manual", val_trimmed)
    return context


def load_context() -> Any:
    """
    Safely load the notification context.
    Tries the official PluginNotificationContext class first,
    falls back to a manual dict from environment variables.
    """
    try:
        from cmk.utils.notify_types import PluginNotificationContext as CMKContext
        if hasattr(CMKContext, "from_env"):
            return _normalize_context(CMKContext.from_env())
    except ImportError:
        pass
    
    # Fallback: Manual reconstruction from NOTIFY_ environment variables
    return _normalize_context({k[7:]: v for k, v in os.environ.items() if k.startswith("NOTIFY_")})


def _get_token(context: Any) -> str:
    """
    Safely retrieve the access token across Checkmk 2.3, 2.4, and 2.5.
    Supports:
      1. Official Checkmk get_password_from_env_or_context / retrieve_from_passwordstore
      2. Direct and flattened PARAMETER_CMK2NTFY_ACCESS_TOKEN keys in context / os.environ
      3. Structured/migrated password tuples (explicit or password store)
      4. Fallback search for token values or password store IDs
    """
    # 1. Try official Checkmk get_password_from_env_or_context helper
    try:
        from cmk.notification_plugins.utils import get_password_from_env_or_context
        token = get_password_from_env_or_context("PARAMETER_CMK2NTFY_ACCESS_TOKEN", context)
        if token:
            return str(token).strip()
        token = get_password_from_env_or_context("CMK2NTFY_ACCESS_TOKEN", context)
        if token:
            return str(token).strip()
    except (ImportError, AttributeError):
        pass
    except Exception as e:
        sys.stderr.write(f"cmk2ntfy: get_password_from_env_or_context failed: {e}\n")

    # 2. Check if a direct token string or tuple is in context["PARAMETER_CMK2NTFY_ACCESS_TOKEN"]
    val = context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN") if isinstance(context, dict) else None

    # Try official retrieve_from_passwordstore if val is present
    if val:
        try:
            from cmk.notification_plugins.utils import retrieve_from_passwordstore
            resolved = retrieve_from_passwordstore(val)
            if resolved:
                return str(resolved).strip()
        except Exception:
            pass

    # 3. If val is not present or unresolved, check flattened keys (e.g. PARAMETER_CMK2NTFY_ACCESS_TOKEN_*)
    # Checkmk flattens ("cmk_postprocessed", "explicit_password", (id, "token")) into:
    # PARAMETER_CMK2NTFY_ACCESS_TOKEN_1 = "cmk_postprocessed"
    # PARAMETER_CMK2NTFY_ACCESS_TOKEN_2 = "explicit_password" or "stored_password"
    # PARAMETER_CMK2NTFY_ACCESS_TOKEN_3 = "uuid...       token"
    # PARAMETER_CMK2NTFY_ACCESS_TOKEN_3_1 = "uuid..." (password store ID or ad-hoc ID)
    # PARAMETER_CMK2NTFY_ACCESS_TOKEN_3_2 = "token" (explicit token)
    env_and_context: dict[str, Any] = {}
    if isinstance(context, dict):
        env_and_context.update(context)
    env_and_context.update({k[7:] if k.startswith("NOTIFY_") else k: v for k, v in os.environ.items()})

    pw_type = env_and_context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN_2")
    explicit_token = env_and_context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN_3_2")
    pw_id = env_and_context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN_3_1")

    # If explicit password was selected
    if explicit_token and isinstance(explicit_token, str) and explicit_token.strip():
        return explicit_token.strip()

    # If stored password from Checkmk Password Store
    if pw_type == "stored_password" or (pw_id and not explicit_token):
        target_id = pw_id or env_and_context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN_3") or env_and_context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN_1")
        if target_id and isinstance(target_id, str):
            target_id = target_id.strip()
            try:
                from cmk.utils import password_store
                extracted = password_store.extract(target_id)
                if extracted:
                    return str(extracted).strip()
            except Exception as ps_err:
                sys.stderr.write(f"cmk2ntfy: password_store.extract({target_id}) failed: {ps_err}\n")

    # 4. Handle serialized tuple / list / ast structures in val or environment
    if not val:
        val = env_and_context.get("PARAMETER_CMK2NTFY_ACCESS_TOKEN")

    if val:
        parsed_val = val
        if isinstance(val, str) and (val.startswith("(") or val.startswith("[")):
            import ast
            try:
                parsed_val = ast.literal_eval(val)
            except Exception as ast_err:
                sys.stderr.write(f"cmk2ntfy: fallback ast parsing failed: {ast_err}\n")
                parsed_val = val

        if isinstance(parsed_val, (tuple, list)):
            try:
                if len(parsed_val) == 3 and parsed_val[0] == "cmk_postprocessed":
                    p_type = parsed_val[1]
                    p_data = parsed_val[2]
                    if p_type == "explicit_password" and isinstance(p_data, (tuple, list)) and len(p_data) == 2:
                        return str(p_data[1]).strip()
                    if p_type == "stored_password" and isinstance(p_data, (tuple, list)) and len(p_data) >= 1:
                        from cmk.utils import password_store
                        return str(password_store.extract(p_data[0])).strip()
                elif len(parsed_val) == 2:
                    if parsed_val[0] == "explicit_password":
                        if isinstance(parsed_val[1], (tuple, list)) and len(parsed_val[1]) == 2:
                            return str(parsed_val[1][1]).strip()
                        return str(parsed_val[1]).strip()
                    if parsed_val[0] == "stored_password":
                        from cmk.utils import password_store
                        p_id = parsed_val[1][0] if isinstance(parsed_val[1], (tuple, list)) else parsed_val[1]
                        return str(password_store.extract(p_id)).strip()
            except Exception as err:
                sys.stderr.write(f"cmk2ntfy: failed extracting password from structure: {err}\n")

        if isinstance(val, str) and not (val.startswith("(") or val.startswith("[")):
            return val.strip()

    # 5. Last fallback: scan all PARAMETER_CMK2NTFY_ACCESS_TOKEN_* variables for non-empty values
    for k, v in env_and_context.items():
        if k.startswith("PARAMETER_CMK2NTFY_ACCESS_TOKEN_") and isinstance(v, str):
            v_clean = v.strip()
            if v_clean not in ("cmk_postprocessed", "explicit_password", "stored_password", ""):
                try:
                    from cmk.utils import password_store
                    extracted = password_store.extract(v_clean)
                    if extracted:
                        return str(extracted).strip()
                except Exception:
                    pass
                if not v_clean.startswith("uuid"):
                    return v_clean

    return ""


class PRIORITY(IntEnum):
    OK = 3
    UP = 3
    WARN = 3
    WARNING = 3
    CRIT = 4
    CRITICAL = 4
    DOWN = 4
    UNREACH = 4
    UNKN = 2


class TAGICON:
    OK = "green_circle"
    UP = "green_circle"
    WARN = "yellow_circle"
    WARNING = "yellow_circle"
    CRIT = "red_circle"
    CRITICAL = "red_circle"
    DOWN = "red_circle"
    UNREACH = "red_circle"
    UNKN = "white_circle"

    @classmethod
    def get(cls, state: str) -> str:
        return getattr(cls, str(state).upper() if state else "UNKN", cls.UNKN)


HOST_MSG_TMPL_FALLBACK = "Host: $HOSTALIAS$ $HOSTSTATE$\n\n$HOSTOUTPUT$"
SVC_MSG_TMPL_FALLBACK = (
    "Service: $HOSTALIAS$ / $SERVICEDESC$ $SERVICESTATE$\n\n$SERVICEOUTPUT$"
)
URL_FALLBACK = "https://ntfy.sh"


def _get_item_url(context: Any) -> str:
    """
    Safely get the host or service URL from Checkmk context,
    supporting manual URL prefixes (e.g. reverse proxy setups).
    """
    what = context.get("WHAT")

    # 1. Check if user configured a custom URL prefix directly in cmk2ntfy_url_prefix
    custom_url_prefix = (
        context.get("PARAMETER_CMK2NTFY_URL_PREFIX")
        or context.get("PARAMETER_CMK2NTFY_URL")
        or context.get("PARAMETER_CMK2NTFY_LINK")
    )
    if custom_url_prefix and isinstance(custom_url_prefix, str) and custom_url_prefix.strip():
        prefix = custom_url_prefix.strip()
        if not prefix.endswith("/"):
            prefix += "/"
        import urllib.parse
        hostname = context.get("HOSTNAME", "")
        if what == "SERVICE":
            servicedesc = context.get("SERVICEDESC", "")
            start_url = f"view.py?view_name=service&host={urllib.parse.quote_plus(hostname)}&service={urllib.parse.quote_plus(servicedesc)}"
        else:
            start_url = f"view.py?view_name=hoststatus&host={urllib.parse.quote_plus(hostname)}"
        return f"{prefix}index.py?start_url={urllib.parse.quote_plus(start_url)}"

    # 2. Try official Checkmk helpers with normalized PARAMETER_URL_PREFIX
    try:
        if what == "SERVICE":
            url = service_url_from_context(context)
            if url:
                return str(url)
        elif what == "HOST":
            url = host_url_from_context(context)
            if url:
                return str(url)
    except Exception as e:
        sys.stderr.write(f"cmk2ntfy: url_from_context failed: {e}\n")

    # 3. Fallback manual URL construction if Checkmk helper fails or is unavailable
    url_prefix_param = context.get("PARAMETER_URL_PREFIX")
    prefix = None
    if isinstance(url_prefix_param, (tuple, list)) and len(url_prefix_param) == 2:
        method, custom_prefix = url_prefix_param
        if method == "manual" and custom_prefix:
            prefix = str(custom_prefix)
        elif method == "automatic_http":
            prefix = f"http://{context.get('MONITORING_HOST', 'localhost')}/{context.get('OMD_SITE', '')}/check_mk/"
        elif method == "automatic_https":
            prefix = f"https://{context.get('MONITORING_HOST', 'localhost')}/{context.get('OMD_SITE', '')}/check_mk/"
    elif isinstance(url_prefix_param, str) and (url_prefix_param.startswith("http://") or url_prefix_param.startswith("https://")):
        prefix = url_prefix_param

    if not prefix:
        site = context.get("OMD_SITE", "")
        host = context.get("MONITORING_HOST", "localhost")
        prefix = f"https://{host}/{site}/check_mk/" if site else f"https://{host}/check_mk/"

    if not prefix.endswith("/"):
        prefix += "/"

    import urllib.parse
    hostname = context.get("HOSTNAME", "")
    if what == "SERVICE":
        servicedesc = context.get("SERVICEDESC", "")
        start_url = f"view.py?view_name=service&host={urllib.parse.quote_plus(hostname)}&service={urllib.parse.quote_plus(servicedesc)}"
    else:
        start_url = f"view.py?view_name=hoststatus&host={urllib.parse.quote_plus(hostname)}"

    return f"{prefix}index.py?start_url={urllib.parse.quote_plus(start_url)}"


def _cmk2ntfy_message_constructor(context: Any) -> dict[str, Any]:
    """Build the message"""

    what = context.get("WHAT")
    if what == "SERVICE":
        state = context.get("SERVICESTATE")
        msg_tmpl = "PARAMETER_CMK2NTFY_SVC_MSG_TMPL"
        msg_tmpl_fallback = SVC_MSG_TMPL_FALLBACK
    elif what == "HOST":
        state = context.get("HOSTSTATE")
        msg_tmpl = "PARAMETER_CMK2NTFY_HOST_MSG_TMPL"
        msg_tmpl_fallback = HOST_MSG_TMPL_FALLBACK
    else:
        sys.stderr.write(f"unknown Type: {what}, expecting: SERVICE or HOST. sending generic message\n")
        state = "UNKN"
        msg_tmpl = ""
        context_as_str = "\n".join([f"{k}: '{v}'" for k, v in context.items()])
        msg_tmpl_fallback = f"UNKNOWN Notification Type: {what}\n\ncontext:\n{context_as_str}"

    item_url = _get_item_url(context)

    # Normalize state to uppercase for case-insensitive matches in PRIORITY and TAGICON
    if state:
        state = str(state).upper()
    else:
        state = "UNKN"

    tag_icon = TAGICON.get(state)
    try:
        priority = int(getattr(PRIORITY, state, PRIORITY.UNKN))
    except (AttributeError, TypeError, ValueError):
        priority = PRIORITY.UNKN

    msg_template_val = context.get(msg_tmpl)
    if not msg_template_val:
        msg_template_val = msg_tmpl_fallback

    try:
        item_title, item_message = str(msg_template_val).split("\n\n", 1)
    except ValueError:
        item_title = str(msg_template_val)
        item_message = ""

    notification_type = context.get("NOTIFICATIONTYPE")
    if notification_type == "ACKNOWLEDGEMENT":
        item_title += " (acknowledged)"
        item_message += f"\n{context.get('NOTIFICATIONAUTHORALIAS', '')} acknowledged with comment: \n{context.get('NOTIFICATIONCOMMENT', '')}"
    elif notification_type == "FLAPPINGSTART":
        item_title += " (started flapping)"
    elif notification_type == "FLAPPINGSTOP":
        item_title += " (stopped flapping)"
    elif notification_type == "CUSTOM":
        item_title += " (via custom command)"
    elif notification_type == "ALERTHANDLER":
        item_title += " (via alerthandler)"

    ntfy_data: dict[str, Any] = {
        "topic": context.get("PARAMETER_CMK2NTFY_TOPIC"),
        "title": substitute_context(item_title, context),
        "message": substitute_context(item_message, context),
        "priority": priority,
        "tags": [tag_icon],
    }

    if item_url and str(item_url).lower().startswith("http"):
        site = context.get("PARAMETER_CMK2NTFY_SITE") or context.get("PARAMETER_CMK2NTFY_SITE_NAME")
        if site:
            site = substitute_context(str(site), context)
        else:
            site = context.get("OMD_SITE") or "OMD_SITE"

        ntfy_data.update(
            {
                "actions": [
                    {
                        "action": "view",
                        "label": f"Open {site}",
                        "url": item_url,
                        "clear": True,
                    },
                ]
            }
        )

    if context.get("PARAMETER_CMK2NTFY_INCLUDE_CMK_ICON"):
        ntfy_data.update({"icon": "https://checkmk.com/favicon.ico"})

    return ntfy_data


def _auth_headers(headers: dict[str, str], context: Any) -> dict[str, str]:
    token = _get_token(context)
    if token and token.strip():
        headers.update({"Authorization": f"Bearer {token}"})
    return headers


def _post_request() -> requests.Response:
    context = load_context()
    
    serialized_proxy_config = context.get("PARAMETER_PROXY_URL")
    verify = "PARAMETER_IGNORE_SSL" not in context

    headers = {}
    headers = _auth_headers(headers, context)

    url = context.get("PARAMETER_CMK2NTFY_INSTANCE", URL_FALLBACK)
    if url and not (str(url).startswith("http://") or str(url).startswith("https://")):
        url = f"https://{url}"

    ntfy_payload = _cmk2ntfy_message_constructor(context)
    
    sys.stderr.write(f"cmk2ntfy: posting to {url} (topic: {ntfy_payload.get('topic')})\n")

    proxies = None
    if serialized_proxy_config:
        try:
            proxies = deserialize_http_proxy_config(serialized_proxy_config).to_requests_proxies()
        except Exception as e:
            sys.stderr.write(f"cmk2ntfy: failed to deserialize proxy configuration: {e}\n")

    try:
        response = requests.post(
            url=url,
            json=ntfy_payload,
            proxies=proxies,
            headers=headers or None,
            verify=verify,
            timeout=10,
        )
        sys.stderr.write(f"cmk2ntfy: response status: {response.status_code}\n")
        sys.stderr.write(f"cmk2ntfy: response body: {response.text}\n")
        return response
    except Exception as e:
        sys.stderr.write(f"cmk2ntfy: error during request: {e}\n")
        raise


def main() -> int:
    try:
        return process_by_status_code(_post_request(), success_code=200)
    except Exception:
        traceback.print_exc(file=sys.stderr)
        return 2


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