#!/usr/bin/env python3

# +------------------------------------------------------------+
# |                                                            |
# |             | |             | |            | |             |
# |          ___| |__   ___  ___| | ___ __ ___ | | __          |
# |         / __| '_ \ / _ \/ __| |/ / '_ ` _ \| |/ /          |
# |        | (__| | | |  __/ (__|   <| | | | | |   <           |
# |         \___|_| |_|\___|\___|_|\_\_| |_| |_|_|\_\          |
# |                                   custom code by SVA       |
# |                                                            |
# +------------------------------------------------------------+
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   Copyright (C) 2025  SVA System Vertrieb Alexander GmbH
#                       by michael.hoess@sva.de + colleagues


import datetime
import hashlib
import os
import traceback
import re
import sys
import time

from typing import Iterable, Optional, Tuple, Literal, Any

from re import Pattern


from cmk import trace


from cmk_addons.plugins.automated_downtimes.lib.tracing_compat import get_tracer



from cmk_addons.plugins.automated_downtimes.lib.common import (
    dbg,
    try_strip_fqdn,
    Downtime,
    NAGRES_OK,
    NAGRES_CRASH,
    parse_args,
    show_config_dump,
    Env,
    configure_tracer,
)

from cmk_addons.plugins.automated_downtimes.lib.lqapi import LqAPI
from cmk_addons.plugins.automated_downtimes.lib.restapi import RestAPI
from cmk_addons.plugins.automated_downtimes.lib.cache import LocalState, LocalStateData, LocalCache, LocalCacheData, GlobalCache

VERSION = "2.5"
HASH_ID = "check_auto_downtimes"

tracer = get_tracer(__name__)

#
#
#



env = Env()

#
# Targetlist building
#


class TargetListBuilder:

    def __init__(
        self,
        matches_case_insensitive: bool,
        strip_fqdn_when_useful: bool,
        hostname_boundary_match: bool,
        # rest_api: RestAPI,
        # lq_api: LqAPI,
        cache: GlobalCache,
    ):

        self._case_insensitive = matches_case_insensitive
        self._allow_fqdn_strip = strip_fqdn_when_useful
        self._hostname_boundary_match = hostname_boundary_match

        self._cache = cache
        # self._rest_api = rest_api Force crash if still used here
        # self._lq_api = lq_api
        self._result: Tuple[Tuple[Any]] = tuple() # type: ignore

        # self._def_api = self._rest_api
        self._def_api = self._cache

    def add(self, target: Tuple[str, str, str]):
        
        """Used for manual specified deps"""
        if not target[2]:
            for ch in self._def_api.find_hosts(
                name_regex=target[1],
                case_insensitive=self._case_insensitive,
            ):
                self._result += ((target[0], ch, ""),) # type: ignore

        else:
            
            for ch in self._def_api.find_services(
                name_regex=target[2],
                host_name_regex=target[1],
                optional_identifier=None,
                case_insensitive=self._case_insensitive,
                boundary_match=False,
            ):
                self._result += ((target[0], ch[0], ch[1]),) # type: ignore

            # self._result += (target,)

    def get(self) -> Tuple[Tuple[str]]:
        return self._result

    def add_childs_from_parent(self, parent_host: str) -> None:
        for ch in self._def_api.find_childs_of_host(
            host_name=parent_host,
            recursive=True,
            case_insensitive=self._case_insensitive,
        ):
 
            self._result += ((f"Auto-detected child host {ch}", ch, ""),) # type: ignore

    def add_dependant_services(self, host_name: str, optional_identifier: str):
        hns = host_name if not self._allow_fqdn_strip else try_strip_fqdn(host_name)
        for hn, svc in self._def_api.find_services(
            hns,
            optional_identifier,
            self._case_insensitive,
            self._hostname_boundary_match,
        ):
            self._result += (("Auto-detected dependent service", hn, svc),) # type: ignore

    def add_dependant_services_on_parent(
        self, host_name: str, optional_identifier: str
    ):
        # TODO: Obsolete, modeis no longer exposed in UI?
        """Find parents of host_name, and there find serivce by name containing the host_name"""

        hns = host_name if not self._allow_fqdn_strip else try_strip_fqdn(host_name)
        parents = self._def_api.find_parents_of_host(host_name, self._case_insensitive)
        for parent in parents:
            pns = parent if not self._allow_fqdn_strip else try_strip_fqdn(parent)
            svc_res = self._def_api.find_services_by_host(pns, optional_identifier)
            for hns, svc_name in svc_res:
                self._result += ( # type: ignore
                    ("Auto-detected dependent service on parent", host_name, svc_name),
                ) 

    def add_myself(self, host_name: str) -> None:
        self._result += (("Auto-detected host itself", host_name, ""),) # type: ignore

    def add_similar_host_names(self, host_name: str) -> None:
        hns = host_name if not self._allow_fqdn_strip else try_strip_fqdn(host_name)
        for h in self._def_api.find_similar_hosts(
            hns, self._case_insensitive, self._hostname_boundary_match
        ):
            self._result += (("Similar hostname", h, ""),) # type: ignore

    @staticmethod
    def safe_create(
        env: Env,
        maintenance_by: str,
        # rest_api: RestAPI,
        global_cache: GlobalCache,
    ) -> Tuple[Tuple[str]]:

        dbg("Building targetlist...")

        tgt_list = TargetListBuilder(
            env.case_insensitive,
            strip_fqdn_when_useful=env.strip_fqdn,
            hostname_boundary_match=env.hostname_boundary_match,
            cache=global_cache,
        )
        dependency_detection = env.dependency_detection
        default_not_found_state = 0 # Was inconsitent for the difference detection modes

        if dependency_detection == "fully_automated":
            if maintenance_by == "service":
                tgt_list.add_myself(env.my_host_name) # type: ignore

            dbg("Tgt-FullyAutomated: Adding similar hosts...")
            tgt_list.add_similar_host_names(env.my_host_name) # type: ignore
            dbg("Tgt-FullyAutomated: Add childs from parent...")
            tgt_list.add_childs_from_parent(env.my_host_name) # type: ignore
            dbg("Tgt-FullyAutomated: Add dependant services...")
            tgt_list.add_dependant_services(env.my_host_name, env.optional_identifier) # type: ignore
            if env.my_host_name == env.monitor_host and not env.optional_identifier:
                env.no_match_msg_tag = ""

        elif dependency_detection == "search_parent_child":
            tgt_list.add_myself(env.my_host_name) # type: ignore
            tgt_list.add_dependant_services(env.my_host_name, env.optional_identifier) # type: ignore
            tgt_list.add_childs_from_parent(env.my_host_name) # type: ignore
            tgt_list.add_dependant_services_on_parent(
                env.my_host_name, env.optional_identifier # type: ignore
            )

        elif dependency_detection == "search_child":
            tgt_list.add_myself(env.my_host_name) # type: ignore
            tgt_list.add_childs_from_parent(env.my_host_name) # type: ignore
            # tgt_list.add_dependant_services(env.my_host_name, env.optional_identifier)

        elif dependency_detection == "specify_targets":
            default_not_found_state = 3 # Mimic old behavior
            for m in env.manual_targets:
                if len(m) < 1 or len(m) > 3:
                    result_set_summary(
                        "! Bad manual target list, invalid num of arguments"
                    )
                    do_exit(NAGRES_CRASH)

                if (len(m) == 2 and m[1]):                                        
                    tgt_list.add((m[0], m[1], ""))
                    continue

                if len(m) == 3 and m[1]:
                    # Hostname and optionally a service
                    tgt_list.add(m)
                    continue

                result_set_summary("! Bad manual target list, host always required")
                do_exit(NAGRES_CRASH)

                # for mm in m:
                #     if not mm:
                #         result_set_summary(
                #             "! Bad manual target list, no blanks allowed!"
                #         )  # This seemd to be ignored for a long time but did no work
                #         do_exit(NAGRES_CRASH)
                # tgt_list.add(m)

        else:
            result_set_summary("! Invalid mode for --dependency_detection")
            do_exit(NAGRES_CRASH)

        if not tgt_list.get():
            nf_state = default_not_found_state
            if env.no_target_found_state is not None:
                nf_state = env.no_target_found_state

            if nf_state > 0:
                result_set_summary(
                    "! No targets found"
                )
                do_exit(nf_state)
            else:
                result_set_summary(
                    "No targets defined or no targets found"
                )
                do_exit(NAGRES_OK)


        dbg("Building targetlist Done")
        dbg(f"Target list updated to: {tgt_list.get()}")
        return tgt_list.get()


#
#
#
# Downtime-Support class
#
#
#
class Downtimes:

    @staticmethod
    def _create_hash() -> Tuple[str, str]:
        """returns a tuple. 2nd element is currently NOT used"""

        myid = f"{HASH_ID}{env.my_host_name}{env.my_svc_name}"
        res_my = hashlib.sha1(myid.encode("UTF-8")).hexdigest()

        return (res_my[:12], "")

    @staticmethod
    def get_hash():
        parts = Downtimes._create_hash() # type: ignore
        return f"{parts[0]}{parts[1]}"

    @staticmethod
    def find(
        dts: Iterable[Downtime],
        host_name: Optional[str] = None,
        svc_name: Optional[str] = None,
        comment_find: Optional[str] = None,
        dt_type: Literal["H", "S", "*"] = "*",
    ) -> list[Downtime]:
        """
        return all matching downtimes. to get only service-downtime
        """
        res = []
        for dt in dts:                  
            if host_name and dt.host_name != host_name:                
                continue
            if svc_name and dt.svc_name != svc_name:                
                continue
            if comment_find and dt.comment.find(comment_find) < 0:                
                continue
            if dt_type == "H" and dt.is_svc_dt:                
                continue
            if dt_type == "S" and not dt.is_svc_dt:                     
                continue

            res.append(dt)

        return res

    @staticmethod
    def get_all(api: RestAPI) -> list[Downtime]:        
        return api.get_downtimes()

    # @staticmethod
    # def add(
    #     api: RestAPI,
    #     host_name: str,
    #     service: Optional[str],
    #     task_info: str,
    #     replace_existing: bool,
    #     curr_dts: Optional[Iterable[Downtime]] = None,
    # ) -> None:
    #     """!!! SINGLE Add, should be not longer used !!!"""

    #     dbg(f"{task_info} (host / service: %s  - %s)" % (host_name, service))
    #     global default_downtime

    #     downtime_hash = Downtimes.get_hash(host_name, service) # type: ignore

    #     typ = "Svc" if service else "Host"
    #     comment = f"MAINT#{downtime_hash} {typ}-DT (set by rule '{env.my_svc_name}@{env.my_host_name}')"

    #     to_replace = []
    #     # Find exisiting downtimes with id
    #     if replace_existing:
    #         if curr_dts:
    #             dts = Downtimes.find(curr_dts, host_name, service, downtime_hash)
    #         else:
    #             dts = api.get_downtimes(host_name, service, downtime_hash)
    #         for dt in dts:
    #             id = dt.id
    #             to_replace.append(id)

    #     if len(to_replace):
    #         comment = comment + f" (Replacing {'/'.join(to_replace)})"

    #     start = datetime.datetime.now()
    #     end = start + datetime.timedelta(minutes=default_downtime)

    #     api.set_downtimes(comment, start, end, [(host_name,)])

    #     if len(to_replace):
    #         time.sleep(
    #             2
    #         )  # CMK will cancel old DT before new is place without delay?????
    #         if not api.delete_downtimes(to_replace):
    #             outcome = "FAILED!!"
    #         else:
    #             outcome = "OK"

    #         dbg(
    #             f"After adding dt for {downtime_hash}: del pre-existing dt {to_replace} {outcome}"
    #         )

    @staticmethod
    def add_all(
        api: RestAPI,
        targets: Iterable[Tuple[str, Optional[Iterable[str]]]],
        task_info: str,
        remove_existing: bool,
    ) -> None:
        dbg(f"{task_info} (targets: {len(targets)})") # type: ignore

        ds = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
        downtime_hash = Downtimes.get_hash()
        comment = f"MAINT#{downtime_hash} $TYP$-DT (set by rule '{env.my_svc_name}@{env.my_host_name} at {ds}UTC')"

        if remove_existing:
            # Note this may not be 100% exact in case new targets appear during a downtime, but
            # otherwise batching needs to be enhanced/split up
            comment += "(Renew)"

        start = datetime.datetime.now()
        end = start + datetime.timedelta(minutes=env.default_downtime)

        tgts = []
        for t in targets:
            if t[1] is None:
                tgts.append(t)
            else:
                tgts.append((t[0], [t[1]]))

        api.set_downtimes(comment, start, end, tgts)

        if remove_existing:
            time.sleep(
                2
            )  # CMK will cancel old DT before new is place without delay?????

            dbg(f"Removing all downtimes with my hash: {downtime_hash}")
            before = start - datetime.timedelta(seconds=5)
            if not api.delete_downtimes_by_keyword("MAINT#" + downtime_hash, before):
                outcome = "FAILED!!"
            else:
                outcome = "OK"

            dbg(f"After adding dt for {downtime_hash}: del pre-existing dt: {outcome}")

    @staticmethod
    def remove(api: RestAPI, host_name: str, svc_name: str, task_info):
        dbg(f"{task_info} (host / service: %s  - %s)" % (host_name, svc_name))

        downtime_hash = Downtimes.get_hash(host_name, svc_name) # type: ignore

        for dt in api.get_downtimes(host_name, svc_name, downtime_hash):
            id = dt.id
            dbg(f"Removing downtime {id}")
            api.delete_downtime(id)

    @staticmethod
    def remove_all_own(api: RestAPI):
        my_hash = Downtimes._create_hash()[0] 
        dbg("Removing all downtimes with my hash: " + my_hash)

        api.delete_downtimes_by_keyword("MAINT#" + my_hash)


#
#
#
#


#
# Result-building Helper-functions {{{
#

result_summary = ""
result_details = []


def result_set_summary(msg: str) -> None:
    global result_summary
    dbg(f"Result-Summary: {msg}")
    result_summary = msg


def result_add_detail(*msg: str) -> None:
    global result_details
    for ln in msg:
        dbg(f"Result-Details: {ln}")
        result_details.append(ln)


def do_exit(exit_code: int) -> None:
    sys.stdout.write(f"{result_summary}\n")
    for ln in result_details:
        sys.stdout.write(f"{ln}\n")

    dbg(f"Exiting with {exit_code}")
    sys.exit(exit_code)


# }}}

@tracer.instrument("automated_downtimes.ensure_valid_global_cache")
def _ensure_valid_global_cache(rest_api: RestAPI) -> Tuple[datetime.datetime, int]:
    glob_ft, glob_age, expired = GlobalCache.get_cache_file_time() # type: ignore
    span = trace.get_current_span()
    span.set_attribute("cache.expired.initial", bool(expired))
    if glob_age is not None:
        span.set_attribute("cache.age.seconds", float(glob_age))
    dbg(f"Global cache age: {glob_age}, expired: {expired}")
    if expired:
        dbg("Global cache expired, trying to reload...")
        cache = GlobalCache(rest_api)
        if not cache.load():
            result_set_summary("Can't load cache, being updated elsewhere?")
            do_exit(0)

    glob_ft, glob_age, expired = GlobalCache.get_cache_file_time() # type: ignore
    span.set_attribute("cache.expired.final", bool(expired))
    if glob_age is not None:
        span.set_attribute("cache.age.seconds", float(glob_age))
    if expired:
        result_set_summary("Cache still invalid, giving up")
        do_exit(1)

    return glob_ft, glob_age # type: ignore



@tracer.instrument("automated_downtimes.ensure_target_list")
def _ensure_tgt_list(rest_api: RestAPI, locd: LocalCacheData, maintenance_by: str):
    span = trace.get_current_span()
    span.set_attribute("maintenance.by", maintenance_by)
    span.set_attribute("target_list.cached", locd.tgt_list is not None)

    if locd.tgt_list is None:
        _ensure_valid_global_cache(rest_api)
        cache = GlobalCache(rest_api)
        if not cache.load():
            result_set_summary("Can't load cache, being updated elsewhere?")
            do_exit(0)

        tgt_list = TargetListBuilder.safe_create(
            env=env,
            maintenance_by=maintenance_by,
            # rest_api=rest_api,
            global_cache=cache,
        )
        locd.tgt_list = tgt_list
    else:
        dbg("Using cached target list...")
        tgt_list = locd.tgt_list

    span.set_attribute("targets.count", len(tgt_list))
    return tgt_list


@tracer.instrument("automated_downtimes.get_local_state")
def _get_local_state(lq_api: LqAPI):   
    lost, age = LocalState.load(
        env.get_my_name(),
    )
    span = trace.get_current_span()

      
    if not lost.normal_check_interval_last_update_ts or (
        (
            datetime.datetime.now(tz=datetime.UTC)
            - lost.normal_check_interval_last_update_ts
        ).total_seconds() > 15 * 60 # Refresh after 15 min
    ):
        dbg(
            f"LocalState: Cleared normal check interval, too old"
        )
        lost.normal_check_interval_last_update_ts = datetime.datetime.now(
            tz=datetime.UTC
        )
        lost.normal_check_interval = None
   
    if lost.normal_check_interval is None:
        normal_check_interval = lq_api.get_service_check_interval(
            env.my_host_name, env.my_svc_name # type: ignore
        )
        if not normal_check_interval:
            result_set_summary(
                "! Unexpected result. Can't find myself? Check configuration. See details."
            )
            result_add_detail(f"My host: {env.my_host_name}")
            result_add_detail(f"My service name {env.my_svc_name}")
            do_exit(NAGRES_CRASH)
        else:
            normal_check_interval = normal_check_interval * 60
            lost.normal_check_interval = int(normal_check_interval)
            lost.normal_check_interval_last_update_ts = datetime.datetime.now(tz=datetime.UTC)
    else:
        dbg("Using cached 'normal-check-interval'")

    dbg(
        "Normal check interval for service '%s' is %s seconds"
        % (env.my_svc_name, lost.normal_check_interval)
    )
    span.set_attribute(
        "service.normal_check_interval.seconds",
        float(lost.normal_check_interval),  # type: ignore[arg-type]
    )

    return lost, age

@tracer.instrument("automated_downtimes.get_local_cache")
def _get_local_cache(glob_ft: datetime.datetime):   
    locd, age = LocalCache.load(
        env.get_my_name(),
        glob_ft,
        env.default_downtime,
        env.cmd_line_hash,
    )
    span = trace.get_current_span()
    span.set_attribute("local_cache.cache_hit", locd is not None)

    if locd is None:
        locd = LocalCacheData(None, None, None)

    
    return locd, age


def _extract_from_perfdata(perfdata: str) -> Tuple[Optional[int], Optional[int], bool]:
    start = perfdata.get(env.perfname_start, None) # type: ignore
    end = perfdata.get(env.perfname_end, None) # type: ignore
    set_dt = perfdata.get(env.perfname_set_dt, None) if env.perfname_set_dt else True # type: ignore

    if not end:
        end = None
        set_dt = False

    return (start, end, set_dt)


def _get_maint_by():
    if (
        env.monitor_host
        and (env.monitor_svc is None)
        and (env.monitor_svc_regex is None)
    ):
        _maintenance_by = "host"
    elif env.monitor_host and env.monitor_svc and env.monitor_svc_regex:
        _maintenance_by = "service"
    elif env.monitor_host and env.monitor_svc:
        # "service" now supports non-output-regex setups
        _maintenance_by = "service"
    else:
        result_set_summary(
            "Config error: 'Monitor host' and/or 'Monitor service' undefined!"
        )
        do_exit(NAGRES_CRASH)
    return _maintenance_by # type: ignore

@tracer.instrument("automated_downtimes.needs_gracetime")
def _needs_gracetime(local_state: LocalStateData, dt_prereqs_met: bool) -> bool:
    """
    Check if we are in grace time of a downtime.
    Returns True if we are in grace time, False otherwise.
    """
    res = False
    delta = 0

    # dbg(
    #     f"Grace time check: was last requried: {local_cache.downtime_was_reqd_at}, PreReqs met: {dt_prereqs_met}, GraceTime: {env.dt_end_gracetime_s}"
    # )
    if dt_prereqs_met:
        local_state.downtime_was_reqd_at = datetime.datetime.now(
            tz=datetime.timezone.utc
        )

    elif env.dt_end_gracetime_s > 0:
        dbg(
            f"Grace time check: Checking if we are in an ended downtime-reqirement. Last required: {local_state.downtime_was_reqd_at}, GraceTime: {env.dt_end_gracetime_s}..."
        )
        # Check if we are in grace time
        if (
            local_state.downtime_was_reqd_at
        ):  
            now = datetime.datetime.now(tz=datetime.timezone.utc)
            delta = (now - local_state.downtime_was_reqd_at).total_seconds()
            if delta < env.dt_end_gracetime_s:
                res = True
        if res:
            dbg("Grace time check: yes we are in grace time")
        else:
            dbg("Grace time check: not required, we can return false")

    span = trace.get_current_span()
    span.set_attribute("grace_time.running", res)
    span.set_attribute("grace_time.configured", env.dt_end_gracetime_s)
    span.set_attribute("grace_time.remaining", delta)

    return res


@tracer.instrument("automated_downtimes.needs_maint (by_host)")
def _needs_maint_by_host(
    lq_api: LqAPI,
    rest_api: RestAPI,
    local_state: LocalStateData,
) -> Tuple[bool, str, Optional[list[Downtime]]]:

    curr_dts: Optional[Iterable[Downtime]] = None
    res = False
    reason = "?"

    if env.monitor_dts:
        reason = "Downtime"
        if env.monitor_host == env.my_host_name:
            dts = lq_api.get_downtimes(env.monitor_host, None, None) # type: ignore
            dbg(f"LQ returned DT={len(dts)} on monitored") # type: ignore
            res = len(dts) > 0 # type: ignore
        else:
            curr_dts = Downtimes.get_all(rest_api)
            #curr_dts = Downtimes.get_all(rest_api)
            for dt in Downtimes.find(curr_dts, host_name=env.monitor_host): # type: ignore
                dbg("Rest returned DT! on monitored")
                res = True
                break

    if not res and len(env.monitor_states) > 0:
        reason = "State"
        state = None
        if env.monitor_host == env.my_host_name:
            state = lq_api.get_host_state(env.monitor_host) # type: ignore
            dbg(f"LQ returned state {state} on monitored")
        else:
            state = rest_api.get_host_state(env.my_host_name) # type: ignore
            dbg(f"Rest returned state {state} on monitored")

        if state is not None:
            res = state in env.monitor_states

    needs_grace = _needs_gracetime(local_state, res)
    res = res or needs_grace

    return (
        res,
        reason + (" (gracetime)" if needs_grace else ""),
        curr_dts,
    )


@tracer.instrument("automated_downtimes.needs_maint (by_svc)")
def _needs_maint_by_svc(
    lq_api: LqAPI,
    rest_api: RestAPI,
    local_state: LocalStateData,
) -> Tuple[bool, str, Optional[list[Downtime]]]:

    curr_dts: Optional[list[Downtime]] = None
    res = False
    reason = "?"

    if env.monitor_dts and not env.monitor_svc_regex:
        reason = "Downtime"
        if env.monitor_host == env.my_host_name:
            dts = lq_api.get_downtimes(env.monitor_host, env.monitor_svc, None) # type: ignore
            dbg(f"LQ returned DT={len(dts)} on monitored") # type: ignore
            res = len(dts) > 0
        else:
            curr_dts = Downtimes.get_all(rest_api)
            for dt in Downtimes.find(
                curr_dts, host_name=env.monitor_host, svc_name=env.monitor_svc # type: ignore
            ):
                dbg("Rest returned DT! on monitored")
                res = True
                break

    elif env.monitor_host and env.monitor_svc_regex:
        reason = "Plugin-output"
        # LqlCheckMaintenanceState = "GET services\nFilter: host_name ~ ^" + monitor_host + "$\nFilter: display_name ~ ^" + monitor_service + "$\nFilter: plugin_output ~ " + monitor_service_regex + "\nColumns: host_name\n"
        srch_res = rest_api.find_hosts_having_a_service(
            env.monitor_host,  # type: ignore
            env.monitor_svc,  # type: ignore
            env.monitor_svc_regex,  # type: ignore
            env.perfname_start is not None,
        )
        hosts_with_active_maint = []
        preg: Pattern = None  # type: ignore

        if (
            env.perfname_start is None
            and env.monitor_svc_regex.find("<START>") >= 0  # type: ignore
            and env.monitor_svc_regex.find("<END>") >= 0  # type: ignore
        ):
            preg = re.compile(env.monitor_svc_regex)  # type: ignore

        for hn, plugin_output, perfdata in srch_res:
            if env.perfname_start:
                start, end, dt = _extract_from_perfdata(perfdata)
                if not dt or not start:
                    continue

                startdt = datetime.datetime.fromtimestamp(start)
                enddt = datetime.datetime.fromtimestamp(end)  # type: ignore
                dbg(f"... found {startdt} {start}, {enddt} {end}") 
                if not (
                    # Add hardcoded gracetime of 10 minutes now.
                    # Gracetime should check-intervals, callhome-transmits, timeoffset between servers.
                    # We don't access service-checktimeout, since we don't load this infos only when maint
                    # is active at a later stage only.
                    (
                        startdt - datetime.timedelta(seconds=600)
                        <= datetime.datetime.now()
                    )
                    and (
                        enddt + datetime.timedelta(seconds=600)
                        >= datetime.datetime.now()
                    )
                ):
                    dbg("... out of planned maintenance time")
                    continue

                reason = "Plugin-output with time-perfdata"

            elif preg:
                dbg("Try finding start/end time im svc output")
                matches = preg.match(plugin_output)
                if not matches:
                    dbg("... no match")
                    continue
                start = matches.groupdict().get("START")
                end = matches.groupdict().get("END")
                try:
                    startdt = datetime.datetime.fromisoformat(start)  # type: ignore
                    enddt = datetime.datetime.fromisoformat(end)  # type: ignore
                except:
                    dbg("... could not parse date, skipping")
                    continue
                dbg(f"... found {startdt}, {enddt}")

                if not (
                    # Add hardcoded gracetime of 10 minutes now.
                    # Gracetime should check-intervals, callhome-transmits, timeoffset between servers.
                    # We don't access service-checktimeout, since we don't load this infos only when maint
                    # is active at a later stage only.
                    (
                        startdt - datetime.timedelta(seconds=600)
                        <= datetime.datetime.now()
                    )
                    and (
                        enddt + datetime.timedelta(seconds=600)
                        >= datetime.datetime.now()
                    )
                ):
                    dbg("... out of planned maintenance time")
                    continue

                reason = "Plugin-output with time-indication"

            hosts_with_active_maint.append(hn)

        ## endfor

        res = len(hosts_with_active_maint) != 0

    ## endif determine hosts_with_active_maint TODO: split up those blocks

    if not res and len(env.monitor_states) > 0:
        reason = "State"
        if env.monitor_host == env.my_host_name:
            state = lq_api.get_service_state(env.monitor_host, env.monitor_svc)  # type: ignore
            dbg(f"LQ returned state {state} on monitored")

        else:
            state = rest_api.get_service_state(env.monitor_host, env.monitor_svc)  # type: ignore
            dbg(f"Rest returned state {state} on monitored")

        if state is not None:
            res = state in env.monitor_states

    needs_grace = _needs_gracetime(local_state, res)
    res = res or needs_grace

    return (res, reason + (" (gracetime)" if needs_grace else ""), curr_dts)


@tracer.instrument("automated_downtimes.needs_maintenance")
def _needs_maintenance(
    lq_api: LqAPI, rest_api: RestAPI, local_state: LocalStateData, maintenance_by: str
) -> Tuple[bool, str, Optional[list[Downtime]]]:  # type: ignore
    #
    # Determine if we need to set a downtime
    #
    span = trace.get_current_span()
    span.set_attribute("maintenance.by", maintenance_by)
    if maintenance_by == "host":
        result = _needs_maint_by_host(lq_api, rest_api, local_state)

    elif maintenance_by == "service":
        result = _needs_maint_by_svc(lq_api, rest_api, local_state)

    else:
        raise ValueError(f"Unsupported maintenance mode: {maintenance_by}")

    span.set_attribute("maintenance.active", bool(result[0]))
    span.set_attribute("maintenance.reason", str(result[1]))
    span.set_attribute("maintenance.prefetched_downtimes", result[2] is not None)
    return result


def main():
    host_svc_list = []

    global env
    env = parse_args(VERSION)
    show_config_dump(env)

    maintenance_by = _get_maint_by()
    root_span = trace.get_current_span()
    root_span.set_attribute("maintenance.by", maintenance_by)
    if env.my_host_name is not None:
        root_span.set_attribute("cmk.host_name", str(env.my_host_name))
    if env.monitor_host is not None:
        root_span.set_attribute("cmk.monitor_host", str(env.monitor_host))
    if env.dependency_detection is not None:
        root_span.set_attribute("cmk.dependency_detection", str(env.dependency_detection))
    root_span.set_attribute("cmk.debug_log", bool(getattr(env, "debug_log", False)))

    lq_api = LqAPI()

    rest_api = RestAPI(
        env.omd_host,
        env.omd_site,  # type: ignore
        env.automation_user,
        env.automation_secret,  # type: ignore
        port=env.omd_port,
        use_ssl=(env.omd_port < 5000 or env.omd_port > 5999),
        verify_ssl=env.verify_ssl,
        no_proxy=env.no_proxy,
        is_gui_user=env.is_ui_user,
        cookie_discriminators=[str(env.automation_secret), str(env.automation_user)],
        # lq=None,
    )

    glob_state_ft, glob_state_age = _ensure_valid_global_cache(rest_api)
    local_cache, lo_cache_age = _get_local_cache(glob_state_ft)
    local_state, lo_state_age = _get_local_state(lq_api)
    tgt_list = _ensure_tgt_list(rest_api, local_cache, maintenance_by)

    # This call checks if we need a maintenance. If it needed to load
    # Downtimes via RESTAPI it returns those as well for furhter usage (optimization)
    maintenance, maint_reason, curr_dts = _needs_maintenance(
        lq_api, rest_api, local_state, maintenance_by
    )

    dt_added = 0
    dt_readded = 0
    dt_removed = 0
    with tracer.span("automated_downtimes.apply_downtime_changes") as span:
        span.set_attribute("targets.count", len(tgt_list))
        span.set_attribute("maintenance.active", maintenance)
        span.set_attribute("maintenance.reason", maint_reason)

        #
        # Determine downtimes to set/remove as needed
        #
        if not maintenance:
            if local_cache.no_active_maint is True:
                dbg(
                    "Condition requires no downtimes anymore, downtimes were previously removed!"
                )

            else:
                dbg(
                    "Condition requires no downtimes anymore, removing automated downtimes!"
                )
                maintenance = False
                curr_dts = curr_dts if curr_dts is not None else Downtimes.get_all(rest_api)

                dt_hash = Downtimes.get_hash()

                for target in tgt_list:
                    _ = target[0]
                    target_host = target[1]  # type: ignore
                    target_service = target[2]  # type: ignore

                    dts = Downtimes.find(curr_dts, target_host, target_service, dt_hash)
                    if len(dts) > 0:  # type: ignore
                        # Now, only used for stats below
                        host_svc_list.append(
                            (
                                "batch_del",
                                target_host,
                                target_service,
                                "Batch removing downtime for finished maintenance",
                            )
                        )
                Downtimes.remove_all_own(rest_api)
                local_cache.no_active_maint = True  # Mark our downtimes removed

        else:
            dbg(
                f"Condition requires downtime (reason {maint_reason}), making sure all downtimes are still running long enough."
            )
            local_cache.no_active_maint = (
                False  # Reset flag, since we may add downtimes now
            )

            curr_dts = curr_dts if curr_dts is not None else Downtimes.get_all(rest_api)
            dt_hash = Downtimes.get_hash()

            # Check if there is a extension needed. In this case
            # we "readd"-all so we can do a batchdlete
            needing_readd = False
            for target in tgt_list:
                _ = target[0]
                target_host = target[1]  # type: ignore
                target_service = target[2]  # type: ignore

                dts = Downtimes.find(curr_dts, target_host, target_service, dt_hash)
                if len(dts) > 1:
                    # Multiple DT, something went wrong, don't try to read
                    # but wait for expiry, so we won't have tons on duplicated
                    # downtimes (may be delete didn't work?!)
                    continue
                elif len(dts) == 1:  # type: ignore
                    dt = dts[0]  # type: ignore
                    curr_ts = int(round(time.time()))
                    dt_end_ts = datetime.datetime.timestamp(dt.end_time)
                    iv = local_state.normal_check_interval if local_state.normal_check_interval else 180
                    if curr_ts > dt_end_ts - (iv * 2):  
                        needing_readd = True
                        break

            for target in tgt_list:
                _ = target[0]
                target_host = target[1]  # type: ignore
                target_service = target[2]  # type: ignore

                # Check if downtime exists:
                # - if dt with exact hash does not exist, set own
                # - if dt with excct hash dies exists
                #   check if those dt is sufficent end-time-wise, otherwise readd
                #   in case multiple own dt exist
                dts = Downtimes.find(curr_dts, target_host, target_service, dt_hash)                
                if len(dts) > 1:
                    # Multiple DT, something went wrong, don't try to read
                    # but wait for expiry, so we won't have tons on duplicated
                    # downtimes (may be delete didn't work?!)
                    continue
                elif len(dts) == 1 and needing_readd:
                    host_svc_list.append(
                        (
                            "readd",
                            target_host,
                            target_service,
                            "(Removing/)Adding downtime for extension",
                        )
                    )
                elif len(dts) == 0:
                    host_svc_list.append(
                        ("add", target_host, target_service, "Adding downtime")
                    )

        #
        # Apply remaining changes to downtimes, update stats
        #
        if host_svc_list:
            # print(host_svc_list)

            # Batch-add all required downtimes
            add_targets = []
            remove_old = False
            for host_svc_entry in reversed(list(set(host_svc_list))):
                mode = host_svc_entry[0]
                if mode not in ["add", "readd"]:
                    continue

                s = host_svc_entry[2]
                if s:
                    add_targets.append((host_svc_entry[1], s))
                else:
                    add_targets.append((host_svc_entry[1], None))

                remove_old = remove_old or mode == "readd"
            if add_targets:
                Downtimes.add_all(rest_api, add_targets, "(re)add", remove_old)

            # Process other entries/update statistics
            for host_svc_entry in reversed(list(set(host_svc_list))):
                if host_svc_entry[0] in ["add", "readd"]:
                    # Already done in batch-op above, count only
                    dt_added += 1 if host_svc_entry[0] == "add" else 0
                    dt_readded += 1 if host_svc_entry[0] == "readd" else 0

                elif host_svc_entry[0] == "batch_del":
                    # Only stats, was already removed via batch-request
                    dt_removed += 1

                else:
                    raise "Unsupported operation type: " + host_svc_entry[0]

        span.set_attribute("downtime.existing", len(curr_dts) if curr_dts is not None else 0)
        span.set_attribute("downtime.added", dt_added)
        span.set_attribute("downtime.readded", dt_readded)
        span.set_attribute("downtime.removed", dt_removed)
        if (dt_added + dt_readded + dt_removed) > 0:                        
            span.update_name("automated_downtimes.apply_downtime_changes[w/changes])")

    #
    # plugin output
    #

    # plugin summary
    deps = f"{len(tgt_list)} dependent(s) found"
    if maintenance:
        result_set_summary(f"Maintenance is active. Reason: {maint_reason}. {deps}.")
    else:
        result_set_summary(f"Maintenance is not active. {deps}.")

    # Build plugin-'affected'info
    if not maintenance:
        result_add_detail(
            "If host enters maintenance these hosts and services are also affected:"
        )
    else:
        result_add_detail("Affected hosts and services by this rule:")

    if tgt_list:
        for target in tgt_list: 
            if target[2]:  # type: ignore
                result_add_detail(
                    "- Service '%s' on host '%s' (%s)"
                    % (target[2], target[1], target[0])  # type: ignore
                )
            else:
                result_add_detail("- Host '%s' (%s)" % (target[1], target[0]))  # type: ignore
    else:
        result_add_detail(
            f"{env.no_match_msg_tag} NOTHING FOUND. Nobody seems to be dependant on this host {env.no_match_msg_tag}"
        )

    result_add_detail(
        f"Stats on last run: targets: {len(tgt_list)}. downtimes: {dt_removed} removed // {dt_added} added // {dt_readded} readded/extended"
    )

    if lo_cache_age is None:
        lo_msg = "Just renewed"
    else:
        lo_msg = f"{int(lo_cache_age/60)} minutes old"
    result_add_detail(f"Instance cache age: {lo_msg}")

    if glob_state_age is not None:
        gm = f"{int(glob_state_age/60)} minutes old"
        result_add_detail(f"Global cache age: {gm}")

    #
    # Done
    #
    LocalCache.write(env.get_my_name(), local_cache, env.cmd_line_hash)
    LocalState.write(env.get_my_name(), local_state)
    do_exit(NAGRES_OK)


### End Main

if not "--jaeger" in sys.argv:
    main()
else:
    try:
        configure_tracer()
        trace_context = trace.extract_context_from_environment(dict(os.environ))

        host_name = sys.argv[sys.argv.index("--host_name") + 1] if "--host_name" in sys.argv else "unknown_host"
        service_name = sys.argv[sys.argv.index("--display_service_name") + 1] if "--display_service_name" in sys.argv else "unknown_service"

        with tracer.span(
            "automated_downtimes.run",
            context=trace_context,
            attributes={
                "cmk.site": os.environ["OMD_SITE"],
                "cmk.plugin": HASH_ID,
                "cmk.host": host_name,
                "cmk.service": service_name,
            },
        ):
            main()
    except Exception:
        result_set_summary("! Exception during execution. See details")
        result_add_detail(*traceback.format_exc().splitlines())
        do_exit(NAGRES_CRASH)
    finally:
        try:
            trace.get_current_tracer_provider().force_flush()
        except ValueError:
            pass
