# Nagstamon - Nagios status monitor for your desktop
# Copyright (C) 2008-2026 Henri Wahl <henri@nagstamon.de> et al.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

from argparse import ArgumentParser
from importlib import resources
import os
import platform
import sys
import configparser
import base64
import zlib
import datetime
from urllib.parse import quote
from collections import OrderedDict
from pathlib import Path

# older Kubuntu has trouble with keyring
# see https://github.com/HenriWahl/Nagstamon/issues/447
# necessary to avoid any import because might result in a segmentation fault
KEYRING = True
if platform.system() == 'Linux':
    if 'ubuntu' in platform.platform().lower():
        if 'XDG_SESSION_DESKTOP' in os.environ:
            if os.environ['XDG_SESSION_DESKTOP'].lower() == 'kde':
                KEYRING = False
if KEYRING:
    import keyring

# instead of calling platform.system() every now and then just do it once here
OS = platform.system()
# needed when OS-specific decisions have to be made, mostly Linux/non-Linux
OS_MACOS = 'Darwin'
OS_WINDOWS = 'Windows'
OS_NON_LINUX = (OS_MACOS, OS_WINDOWS)

# simple Wayland detection
if 'WAYLAND_DISPLAY' in os.environ or \
        'XDG_SESSION_TYPE' in os.environ and os.environ['XDG_SESSION_TYPE'] == 'wayland':
    # dirty workaround to activate X11 support in Wayland environment - just a test
    if not os.environ.get('QT_QPA_PLATFORM'):
        os.environ['QT_QPA_PLATFORM'] = 'xcb'
    DESKTOP_WAYLAND = True
else:
    DESKTOP_WAYLAND = False

# detection of somehow quirky desktop enviroments which might need a fix
QUIRKY_DESKTOPS = ('cinnamon', 'gnome-flashback-metacity')
if os.environ.get('CINNAMON_VERSION') or \
        os.environ.get('DESKTOP_SESSION') in QUIRKY_DESKTOPS or \
        os.environ.get('XDG_SESSION_DESKTOP') in QUIRKY_DESKTOPS:
    DESKTOP_NEEDS_FIX = True
else:
    DESKTOP_NEEDS_FIX = False

# use QT platform plugins if not set otherwise on GNOME desktop
# based on QGnomePlatform
if not os.environ.get('QT_QPA_PLATFORMTHEME'):
    if os.environ.get('XDG_SESSION_DESKTOP'):
        if os.environ['XDG_SESSION_DESKTOP'].lower() == 'gnome':
            os.environ['QT_QPA_PLATFORMTHEME'] = 'gnome'

# queue.Queue() needs threading module which might be not such a good idea to be used
# because QThread is already in use
# maybe not the most logical place here to be defined but at least all
# modules access config.py so it can be distributed from here
debug_queue = list()

# temporary dict for string-to-bool-conversion
# the bool:bool relations are thought to make things easier in Dialog_Settings.ok()
BOOLPOOL = {'False': False,
            'True': True,
            False: False,
            True: True}

# config settings which should always be strings, never converted to integer or bool
CONFIG_STRINGS = ['custom_browser',
                  'debug_file',
                  'notification_custom_sound_warning',
                  'notification_custom_sound_critical',
                  'notification_custom_sound_down',
                  'notification_action_warning_string',
                  'notification_action_critical_string',
                  'notification_action_down_string',
                  'notification_action_ok_string',
                  'notification_custom_action_string',
                  'notification_custom_action_separator',
                  're_host_pattern',
                  're_service_pattern',
                  're_status_information_pattern',
                  're_duration_pattern',
                  're_attempt_pattern',
                  're_groups_pattern',
                  're_criticality_pattern',
                  'font',
                  'defaults_acknowledge_comment',
                  'defaults_submit_check_result_comment',
                  'defaults_downtime_comment',
                  'name',
                  'monitor_url',
                  'monitor_cgi_url',
                  'username',
                  'password',
                  'proxy_address',
                  'proxy_username',
                  'proxy_password',
                  'autologin_key',
                  'custom_cert_ca_file',
                  'idp_ecp_endpoint',
                  'monitor_site'
                  ]


class AppInfo:
    """
    contains app information previously located in GUI.py
    """
    NAME = 'Nagstamon'
    VERSION = '3.18.2'
    WEBSITE = 'https://nagstamon.de'
    COPYRIGHT = '©2008-2026 Henri Wahl et al.'
    COMMENTS = 'Nagios status monitor for your desktop'
    # dict of servers to offer for downloads if an update is available
    DOWNLOAD_SERVERS = {'nagstamon.de': 'https://github.com/HenriWahl/Nagstamon/releases'}
    # version URL depends on version string
    if 'alpha' in VERSION.lower() or \
            'beta' in VERSION.lower() or \
            'rc' in VERSION.lower() or \
            '-' in VERSION.lower():
        VERSION_URL = WEBSITE + '/version/unstable'
        VERSION_PATH = '/version/unstable'
    else:
        VERSION_URL = WEBSITE + '/version/stable'
        VERSION_PATH = '/version/stable'


class Config:
    """
    the place for central configuration.
    """

    def __init__(self):
        """
        read config file and set the appropriate attributes
        supposed to be sensible defaults
        """
        # move from minute interval to seconds
        self.update_interval_seconds = 60
        self.short_display = False
        self.long_display = True
        self.show_tooltips = True
        self.show_grid = True
        self.grid_use_custom_intensity = False
        self.grid_alternation_intensity = 10
        self.highlight_new_events = True
        self.default_sort_field = 'status'
        self.default_sort_order = 'descending'
        self.filter_all_down_hosts = False
        self.filter_all_unreachable_hosts = False
        self.filter_all_unreachable_services = False
        self.filter_all_flapping_hosts = False
        self.filter_all_unknown_services = False
        self.filter_all_information_services = False
        self.filter_all_warning_services = False
        self.filter_all_average_services = False
        self.filter_all_high_services = False
        self.filter_all_critical_services = False
        self.filter_all_disaster_services = False
        self.filter_all_flapping_services = False
        self.filter_acknowledged_hosts_services = False
        self.filter_hosts_services_disabled_notifications = False
        self.filter_hosts_services_disabled_checks = False
        self.filter_hosts_services_maintenance = False
        self.filter_services_on_acknowledged_hosts = False
        self.filter_services_on_down_hosts = False
        self.filter_services_on_hosts_in_maintenance = False
        self.filter_services_on_unreachable_hosts = False
        self.filter_hosts_in_soft_state = False
        self.filter_services_in_soft_state = False
        self.position_x = 30
        self.position_y = 30
        self.position_width = 640
        self.position_height = 480
        self.popup_details_hover = True
        self.popup_details_clicking = False
        self.close_details_hover = True
        self.close_details_clicking = False
        self.close_details_clicking_somewhere = False
        self.connect_by_host = True
        self.connect_by_dns = False
        self.connect_by_ip = False
        self.use_default_browser = True
        self.use_custom_browser = False
        self.custom_browser = ''
        self.debug_mode = False
        self.debug_to_file = False
        self.debug_file = os.path.expanduser('~') + os.sep + "nagstamon.log"
        self.check_for_new_version = True
        self.notification = True
        self.notification_flashing = True
        self.notification_desktop = False
        self.notification_actions = False
        self.notification_sound = True
        self.notification_sound_repeat = False
        self.notification_default_sound = True
        self.notification_custom_sound = False
        self.notification_custom_sound_warning = ''
        self.notification_custom_sound_critical = ''
        self.notification_custom_sound_down = ''
        self.notification_action_warning = False
        self.notification_action_warning_string = ''
        self.notification_action_critical = False
        self.notification_action_critical_string = ''
        self.notification_action_down = False
        self.notification_action_down_string = ''
        self.notification_action_ok = False
        self.notification_action_ok_string = ''
        self.notification_custom_action = False
        self.notification_custom_action_string = ''
        self.notification_custom_action_separator = '|'
        self.notification_custom_action_single = True
        self.notify_if_up = False
        self.notify_if_information = True
        self.notify_if_warning = True
        self.notify_if_average = True
        self.notify_if_high = True
        self.notify_if_critical = True
        self.notify_if_disaster = True
        self.notify_if_unknown = True
        self.notify_if_unreachable = True
        self.notify_if_down = True
        # Regular expression filters
        self.re_host_enabled = False
        self.re_host_pattern = ''
        self.re_host_reverse = False
        self.re_service_enabled = False
        self.re_service_pattern = ''
        self.re_service_reverse = False
        self.re_status_information_enabled = False
        self.re_status_information_pattern = ''
        self.re_status_information_reverse = False
        self.re_duration_enabled = False
        self.re_duration_pattern = ''
        self.re_duration_reverse = False
        self.re_attempt_enabled = False
        self.re_attempt_pattern = ''
        self.re_attempt_reverse = False
        self.re_groups_enabled = False
        self.re_groups_pattern = ''
        self.re_groups_reverse = False
        self.color_ok_text = self.default_color_ok_text = '#FFFFFF'
        self.color_ok_background = self.default_color_ok_background = '#006400'
        self.color_information_text = self.default_color_information_text = "#000000"
        self.color_information_background = self.default_color_information_background = "#7499FF"
        self.color_warning_text = self.default_color_warning_text = "#000000"
        self.color_warning_background = self.default_color_warning_background = '#FFFF00'
        self.color_average_text = self.default_color_average_text = "#000000"
        self.color_average_background = self.default_color_average_background = "#FFA059"
        self.color_high_text = self.default_color_high_text = "#000000"
        self.color_high_background = self.default_color_high_background = "#E97659"
        self.color_unknown_text = self.default_color_unknown_text = '#000000'
        self.color_unknown_background = self.default_color_unknown_background = '#FFA500'
        self.color_critical_text = self.default_color_critical_text = '#FFFFFF'
        self.color_critical_background = self.default_color_critical_background = '#FF0000'
        self.color_disaster_text = self.default_color_disaster_text = "#FFFFFF"
        self.color_disaster_background = self.default_color_disaster_background = "#660000"
        self.color_unreachable_text = self.default_color_unreachable_text = '#FFFFFF'
        self.color_unreachable_background = self.default_color_unreachable_background = '#8B0000'
        self.color_down_text = self.default_color_down_text = '#FFFFFF'
        self.color_down_background = self.default_color_down_background = '#000000'
        self.color_error_text = self.default_color_error_text = '#000000'
        self.color_error_background = self.default_color_error_background = '#D3D3D3'
        self.statusbar_floating = True
        self.icon_in_systray = False
        self.windowed = False
        self.fullscreen = False
        self.fullscreen_display = 0
        self.systray_offset_use = False
        self.systray_offset = 10
        self.hide_macos_dock_icon = False
        # as default enable on Linux Desktops like Cinnamon and Gnome Flashback
        if DESKTOP_NEEDS_FIX:
            self.enable_position_fix = True
        else:
            self.enable_position_fix = False
        self.font = ''
        self.defaults_acknowledge_sticky = False
        self.defaults_acknowledge_send_notification = False
        self.defaults_acknowledge_persistent_comment = False
        self.defaults_acknowledge_all_services = False
        self.defaults_acknowledge_expire = False
        self.defaults_acknowledge_expire_duration_hours = 2
        self.defaults_acknowledge_expire_duration_minutes = 0
        self.defaults_acknowledge_comment = 'acknowledged'
        self.defaults_submit_check_result_comment = 'check result submitted'
        self.defaults_downtime_duration_hours = 2
        self.defaults_downtime_duration_minutes = 0
        self.defaults_downtime_comment = 'scheduled downtime'
        self.defaults_downtime_type_fixed = True
        self.defaults_downtime_type_flexible = False
        # internal flag to determine if keyring is available at all - defaults to False
        # use_system_keyring is checked and defined some lines later after config file was read
        self.keyring_available = False
        # setting for keyring usage - might cause trouble on Linux so disable it there as default to avoid crash at start
        if OS in OS_NON_LINUX:
            self.use_system_keyring = True
        else:
            self.use_system_keyring = False

        # Special FX
        # Centreon
        self.re_criticality_enabled = False
        self.re_criticality_pattern = ''
        self.re_criticality_reverse = False

        # the app is unconfigured by default and will stay so if it
        # would not find a config file
        self.unconfigured = True

        # get CLI arguments
        parser = ArgumentParser(prog='nagstamon')
        # mitigate session restore problem https://github.com/HenriWahl/Nagstamon/issues/878
        if len(sys.argv) == 2 or len(sys.argv) >= 6:
            # only add configdir if it might be included at all
            parser.add_argument('configdir', default=None)
        # -session and -name are used by X11 session management and could be
        # safely ignored because nagstamon keeps its own session info
        parser.add_argument('-session', default=None, required=False)
        parser.add_argument('-name', default=None, required=False)
        arguments, unknown_arguments = parser.parse_known_args()
        if 'configdir' in arguments:
            self.configdir = arguments.configdir
        # otherwise if there exists a configdir in current working directory it should be used
        elif os.path.exists(os.getcwd() + os.sep + 'nagstamon.config'):
            self.configdir = os.getcwd() + os.sep + 'nagstamon.config'
        else:
            # ~/.nagstamon/nagstamon.conf is the user conf file
            # os.path.expanduser('~') finds out the user HOME dir where
            # nagstamon expects its conf file to be
            self.configdir = os.path.expanduser('~') + os.sep + '.nagstamon'

        self.configfile = self.configdir + os.sep + 'nagstamon.conf'

        # make path fit for actual os, normcase for letters and normpath for path
        self.configfile = os.path.normpath(os.path.normcase(self.configfile))

        # because the name of the configdir is also stored in the configfile
        # there may be situations where the name gets overwritten by a
        # wrong name so it will be stored here temporarily
        configdir_temp = self.configdir

        # default settings dicts
        self.servers = dict()
        self.actions = dict()

        if os.path.exists(self.configfile):
            # instantiate a configparser to parse the conf file
            # SF.net bug #3304423 could be fixed with allow_no_value argument which
            # is only available since Python 2.7
            # since Python 3 '%' will be interpolated by default which crashes
            # with some URLs
            config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
            config.read(self.configfile)

            # go through all sections of the conf file
            for section in config.sections():
                # go through all items of each sections (in fact there is only on
                # section which has to be there to comply to the .INI file standard

                for i in config.items(section):
                    # omit config file info as it makes no sense to store its path
                    if not i[0] in ('configfile', 'configdir'):
                        # create a key of every config item with its appropriate value
                        # check first if it is a bool value and convert string if it is
                        if i[1] in BOOLPOOL:
                            object.__setattr__(self, i[0], BOOLPOOL[i[1]])
                        # in case there are numbers intify them to avoid later conversions
                        # treat negative value specially as .isdecimal() will not detect it
                        elif i[1].isdecimal() or \
                                (i[1].startswith('-') and i[1].split('-')[1].isdecimal()):
                            object.__setattr__(self, i[0], int(i[1]))
                        else:
                            object.__setattr__(self, i[0], i[1])

            # because the switch from Nagstamon 1.0 to 1.0.1 brings the use_system_keyring property
            # and all the thousands 1.0 installations do not know it yet it will be more comfortable
            # for most of the Windows users if it is only defined as False after it was checked
            # from config file
            if 'use_system_keyring' not in self.__dict__.keys():
                if self.unconfigured is True:
                    # an unconfigured system should start with no keyring to prevent crashes
                    self.use_system_keyring = False
                else:
                    # a configured system seemed to be able to run and thus use system keyring
                    if OS in OS_NON_LINUX:
                        self.use_system_keyring = True
                    else:
                        self.use_system_keyring = self.KeyringAvailable()

            # reset self.configdir to temporarily saved value in case it differs from
            # the one read from configfile and so it would fail to save next time
            self.configdir = configdir_temp

            # servers configuration...
            self.servers = self._load_servers_multiple_config()
            # ... and actions
            self.actions = self.load_multiple_config("actions", "action", "Action")

            # seems like there is a config file so the app is not unconfigured anymore
            self.unconfigured = False

        # Load actions if Nagstamon is not unconfigured, otherwise load defaults
        if self.unconfigured is True:
            self.actions = self._default_actions()

        # do some conversion stuff needed because of config changes and code cleanup
        self._legacy_adjustments()

    def _load_servers_multiple_config(self):
        """
        Loads the servers configuration with special handling for obfuscated passwords.

        This method loads server configuration entries from files, handling deobfuscation
        of usernames and passwords as needed. It supports migration from legacy formats,
        integration with the system keyring for secure password storage, and conversion
        of server types and settings for compatibility with newer versions.

        Returns:
            dict: A dictionary mapping server names to their configuration objects.

        Error Handling:
            Any exceptions during loading or deobfuscation are caught, a stack trace is printed,
            and loading continues for other entries.

        Special Considerations:
            - Usernames and passwords are deobfuscated or retrieved from the system keyring if enabled.
            - Legacy server types and settings are migrated to current formats.
            - Handles both monitor and proxy credentials, as well as optional autologin keys.
            - Ensures server names are always strings for consistency.
        """
        self.keyring_available = self.is_keyring_available()

        servers = self.load_multiple_config('servers', 'server', 'Server')
        # deobfuscate username + password inside a try-except loop
        # if entries have not been obfuscated yet this action should raise an error
        # and old values (from nagstamon < 0.9.0) stay and will be converted when next
        # time saving config
        try:
            for server in servers:
                # server name needs to be a string at every cost
                # see issue https://github.com/HenriWahl/Nagstamon/issues/1010
                servers[server].name = str(servers[server].name)
                # usernames for monitor server and proxy
                servers[server].username = self.deobfuscate(servers[server].username)
                servers[server].proxy_username = self.deobfuscate(servers[server].proxy_username)
                # passwords for monitor server and proxy
                if servers[server].save_password == 'False':
                    servers[server].password = ""
                elif self.keyring_available and self.use_system_keyring:
                    password = keyring.get_password('Nagstamon', '@'.join((servers[server].username,
                                                                           servers[server].monitor_url))) or ""
                    if password == "":
                        if servers[server].password != "":
                            servers[server].password = self.deobfuscate(servers[server].password)
                    else:
                        servers[server].password = password
                elif servers[server].password != "":
                    servers[server].password = self.deobfuscate(servers[server].password)
                # proxy password
                if self.keyring_available and self.use_system_keyring:
                    proxy_password = keyring.get_password('Nagstamon', '@'.join(('proxy',
                                                                                 servers[server].proxy_username,
                                                                                 servers[server].proxy_address))) or ""
                    if proxy_password == "":
                        if servers[server].proxy_password != "":
                            servers[server].proxy_password = self.deobfuscate(servers[server].proxy_password)
                    else:
                        servers[server].proxy_password = proxy_password
                elif servers[server].proxy_password != "":
                    servers[server].proxy_password = self.deobfuscate(servers[server].proxy_password)

                # do only deobfuscating if any autologin_key is set - will be only Centreon/Thruk
                if 'autologin_key' in servers[server].__dict__.keys():
                    if len(servers[server].__dict__['autologin_key']) > 0:
                        servers[server].autologin_key = self.deobfuscate(servers[server].autologin_key)

                # only needed for those who used Icinga2 before it became IcingaWeb2
                if servers[server].type == 'Icinga2':
                    servers[server].type = 'IcingaWeb2'

                # Check_MK is now Checkmk - same renaming as with IcingaWeb2
                if servers[server].type == 'Check_MK Multisite':
                    servers[server].type = 'Checkmk Multisite'
                if 'check_mk_view_hosts' in servers[server].__dict__.keys():
                    servers[server].checkmk_view_hosts = servers[server].check_mk_view_hosts
                    servers[server].__dict__.pop('check_mk_view_hosts')
                if 'check_mk_view_services' in servers[server].__dict__.keys():
                    servers[server].checkmk_view_services = servers[server].check_mk_view_services
                    servers[server].__dict__.pop('check_mk_view_services')

        except Exception:
            import traceback
            traceback.print_exc(file=sys.stdout)

        return servers

    def load_multiple_config(self, settingsdir, setting, configobj):
        """
        Loads configuration entries from files in a given settings directory and returns them as a dictionary.

        This method scans the specified directory for configuration files matching the expected naming pattern,
        parses each file, and creates an object for each configuration entry. It converts string values to
        booleans or integers where appropriate and handles legacy file cleanup if necessary.

        Args:
            settingsdir (str): Name of the subdirectory containing the configuration files (e.g., 'servers' or 'actions').
            setting (str): Prefix used in the configuration file names (e.g., 'server' or 'action').
            configobj (str): Name of the class to instantiate for each configuration entry.

        Returns:
            OrderedDict: A dictionary mapping configuration entry names to their corresponding objects.

        Error Handling:
            Any exceptions during loading are caught, a stack trace is printed, and loading continues for other files.

        Special Considerations:
            - Only files matching the expected naming pattern are processed.
            - Legacy or duplicate files are deleted if found.
            - String values are converted to bool or int where possible for consistency.
        """
        # defaults as empty dict in case settings dir/files could not be found
        settings = OrderedDict()

        try:
            if os.path.exists(self.configdir + os.sep + settingsdir):
                for settingsfile in sorted(os.listdir(self.configdir + os.sep + settingsdir)):
                    if settingsfile.startswith(setting + '_') and settingsfile.endswith('.conf'):
                        config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
                        config.read(self.configdir + os.sep + settingsdir + os.sep + settingsfile)

                        # create object for every setting
                        name = config.get(config.sections()[0], 'name')
                        settings[name] = globals()[configobj]()

                        # go through all items of the server
                        for i in config.items(config.sections()[0]):
                            # create a key of every config item with its appropriate value
                            if i[1] in BOOLPOOL:
                                value = BOOLPOOL[i[1]]
                            # in case there are numbers intify them to avoid later conversions
                            # treat negative value specially as .isdecimal() will not detect it
                            elif i[1].isdecimal() or \
                                    (i[1].startswith('-') and i[1].split('-')[1].isdecimal()):
                                value = int(i[1])
                            else:
                                value = i[1]
                            settings[name].__setattr__(i[0], value)

                        # if filename is still one of the non-URL-ones delete duplicate file
                        if settingsfile != '{0}_{1}.conf'.format(setting, quote(name, safe='')):
                            self.delete_file(settingsdir, settingsfile)
                            # set flag to store the settings via legacy adjustments
                            self.save_config_after_urlencode = True

        except Exception:
            import traceback
            traceback.print_exc(file=sys.stdout)

        return settings

    def save_config(self):
        """
        Saves the current configuration to the configuration file.

        This method writes all current settings of the Config object to the main configuration file,
        as well as to separate files for server and action configurations. Before saving, it ensures
        that the configuration directory exists. Passwords and sensitive data are encrypted or stored
        in the system keyring, depending on the platform and settings.

        Error Handling:
            Exceptions during saving are caught, a stack trace is printed, and debug information is
            logged if debug mode is enabled.

        Special Considerations:
            - The configuration directory is created if it does not exist.
            - Server and action configurations are saved in their own subdirectories.
            - Use of the system keyring is considered based on platform and configuration.
            - Debug information is logged when debug mode is active.
        """
        try:
            # Make sure .nagstamon is created
            if not os.path.exists(self.configdir):
                os.makedirs(self.configdir)
            # save config file with configparser
            config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
            # general section for Nagstamon
            config.add_section('Nagstamon')
            for option in self.__dict__:
                if option not in ['servers', 'actions', 'configfile', 'configdir', 'cli_args']:
                    config.set('Nagstamon', option, str(self.__dict__[option]))

            # because the switch from Nagstamon 1.0 to 1.0.1 brings the use_system_keyring property
            # and all the thousands 1.0 installations do not know it yet it will be more comfortable
            # for most of the Windows users if it is only defined as False after it was checked
            # from config file
            if 'use_system_keyring' not in self.__dict__.keys():
                if self.unconfigured is True:
                    # an unconfigured system should start with no keyring to prevent crashes
                    self.use_system_keyring = False
                else:
                    # a configured system seemed to be able to run and thus use system keyring
                    if OS in OS_NON_LINUX:
                        self.use_system_keyring = True
                    else:
                        self.use_system_keyring = self.is_keyring_available()

            # save servers dict
            self.save_multiple_config('servers', 'server')

            # save actions dict
            self.save_multiple_config('actions', 'action')

            # open, save and close config file
            with open(os.path.normpath(self.configfile), 'w') as file:
                config.write(file)

            # debug
            if self.debug_mode:
                debug_queue.append('DEBUG: {0} Saving configuration to file {1}'.format(str(datetime.datetime.now()),
                                                                                        self.configfile))
        except Exception as err:
            import traceback
            traceback.print_exc(file=sys.stdout)
            # debug
            if self.debug_mode:
                debug_queue.append(
                    'ERROR: {0} {1} while saving configuration to file {2}'.format(str(datetime.datetime.now()),
                                                                                   err,
                                                                                   self.configfile))

    def save_multiple_config(self, settingsdir, setting):
        """
        Saves configuration files for settings such as actions or servers in dedicated subdirectories.

        This method iterates over the specified settings dictionary (e.g., servers or actions) and writes
        each configuration entry to its own file within a subdirectory. Sensitive data such as passwords
        are obfuscated or stored in the system keyring if enabled. The method ensures that the target
        directory exists before saving and handles platform-specific keyring integration.

        Args:
            settingsdir (str): Name of the subdirectory where configuration files will be saved (e.g., 'servers' or 'actions').
            setting (str): Prefix used for the configuration file names (e.g., 'server' or 'action').

        Error Handling:
            Exceptions during saving are caught and printed to the standard output. If password saving to
            the keyring fails, the application will exit to prevent insecure storage.

        Special Considerations:
            - Each configuration entry is saved in a separate file for modularity.
            - Passwords and sensitive fields are obfuscated or stored securely.
            - The method creates the target directory if it does not exist.
            - Old or duplicate configuration files may be cleaned up as needed.
        """

        # only import keyring lib if configured to do so - to avoid Windows crashes
        # like https://github.com/HenriWahl/Nagstamon/issues/97
        if self.use_system_keyring is True:
            self.keyring_available = self.is_keyring_available()

        # one section for each setting
        for s in self.__dict__[settingsdir]:
            config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
            config.add_section(setting + '_' + s)
            for option in self.__dict__[settingsdir][s].__dict__:
                # obfuscate certain entries in config file - special arrangement for servers
                if settingsdir == 'servers':
                    if option in ['username', 'password', 'proxy_username', 'proxy_password', 'autologin_key']:
                        value = self.obfuscate(self.__dict__[settingsdir][s].__dict__[option])
                        if option == 'password':
                            if self.__dict__[settingsdir][s].save_password is False:
                                value = ''
                            elif self.keyring_available and self.use_system_keyring:
                                if self.__dict__[settingsdir][s].password != '':
                                    # provoke crash if password saving does not work - this is the case
                                    # on newer Ubuntu releases
                                    try:
                                        keyring.set_password('Nagstamon',
                                                             '@'.join((self.__dict__[settingsdir][s].username,
                                                                       self.__dict__[settingsdir][s].monitor_url)),
                                                             self.__dict__[settingsdir][s].password)
                                    except Exception:
                                        import traceback
                                        traceback.print_exc(file=sys.stdout)
                                        sys.exit(1)
                                value = ''
                        if option == 'proxy_password':
                            if self.keyring_available and self.use_system_keyring:
                                if self.__dict__[settingsdir][s].proxy_password != '':
                                    # provoke crash if password saving does not work - this is the case
                                    # on newer Ubuntu releases
                                    try:
                                        keyring.set_password('Nagstamon',
                                                             '@'.join(('proxy',
                                                                       self.__dict__[settingsdir][s].proxy_username,
                                                                       self.__dict__[settingsdir][s].proxy_address)),
                                                             self.__dict__[settingsdir][s].proxy_password)
                                    except Exception:
                                        import traceback
                                        traceback.print_exc(file=sys.stdout)
                                        sys.exit(1)

                                value = ''
                        config.set(setting + '_' + s, option, str(value))
                    else:
                        config.set(setting + '_' + s, option, str(self.__dict__[settingsdir][s].__dict__[option]))
                else:
                    config.set(setting + '_' + s, option, str(self.__dict__[settingsdir][s].__dict__[option]))

            # open, save and close config_server file
            if not os.path.exists(self.configdir + os.sep + settingsdir):
                os.makedirs(self.configdir + os.sep + settingsdir)

            # quote strings and thus avoid invalid characters
            with open(
                    os.path.normpath('{0}{1}{2}{1}{3}_{4}.conf'.format(self.configdir,
                                                                       os.sep,
                                                                       settingsdir,
                                                                       setting,
                                                                       quote(s, safe=''))),
                    'w') as file:
                config.write(file)

            # ### clean up old deleted/renamed config files
            # ##if os.path.exists(self.configdir + os.sep + settingsdir):
            # ##    for f in os.listdir(self.configdir + os.sep + settingsdir):
            # ##        if not f.split(setting + "_")[1].split(".conf")[0] in self.__dict__[settingsdir]:
            # ##            os.unlink(self.configdir + os.sep + settingsdir + os.sep + f)

    def is_keyring_available(self):
        """
        Determines if the keyring module and a suitable backend are available for secure password storage.

        This method checks whether the keyring library is installed and whether a supported keyring backend
        is accessible on the current platform. It handles platform-specific logic to avoid known issues,
        such as crashes on certain Linux distributions. If keyring is available and functional, the method
        returns True; otherwise, it returns False.

        Returns:
            bool: True if a working keyring backend is available, False otherwise.

        Error Handling:
            Any exceptions during the check are caught, a stack trace is printed, and the method returns False.

        Special Considerations:
            - On Linux, keyring is only used if it is provided by the distribution to avoid compatibility issues.
            - The method avoids importing keyring unless necessary to prevent crashes on some platforms.
            - Designed to be robust against missing dependencies or misconfigured environments.
        """
        try:
            # Linux systems should use keyring only if it comes with the distro, otherwise chances are small
            # that keyring works at all
            if OS in OS_NON_LINUX:
                # safety first - if not yet available disable it
                if 'use_system_keyring' not in self.__dict__.keys():
                    self.use_system_keyring = False
                # only import keyring lib if configured to do so
                # necessary to avoid Windows crashes like https://github.com/HenriWahl/Nagstamon/issues/97
                if self.use_system_keyring:
                    # hint for packaging: nagstamon.spec always have to match module path
                    # keyring has to be bound to object to be used later
                    import keyring
                    return not (keyring.get_keyring() is None)
                else:
                    return False
            elif KEYRING:
                # keyring and secretstorage have to be importable
                import keyring
                # import secretstorage module as dependency of keyring -
                # if not available keyring won't work
                import secretstorage
                if keyring.get_keyring():
                    return True
                else:
                    return False
            else:
                # apparently an Ubuntu KDE session which has problems with keyring
                return False
        except Exception:
            import traceback
            traceback.print_exc(file=sys.stdout)
            return False

    def obfuscate(self, string, count=5):
        """
        Obfuscates a given string to securely store sensitive information such as passwords.

        Args:
            string (str): The input string to be obfuscated (e.g., a password).
            count (int, optional): The number of obfuscation rounds to apply. Default is 5.

        Returns:
            str: The obfuscated string, encoded as a base64 Unicode string.

        Usage:
            Use this method to obfuscate passwords or other sensitive fields before saving them
            to disk. To retrieve the original value, use the corresponding DeObfuscate method.
        """

        string = string.encode()

        for i in range(count):
            string = base64.b64encode(string).decode()
            string = list(string)
            string.reverse()
            string = "".join(string)
            string = string.encode()
            string = zlib.compress(string)

        # make unicode of bytes string
        string = base64.b64encode(string).decode()
        return string

    def deobfuscate(self, string, count=5):
        """
        Deobfuscates a previously obfuscated string to retrieve the original sensitive information.

        Args:
            string (str): The obfuscated string to be deobfuscated.
            count (int, optional): The number of deobfuscation rounds to apply. Default is 5.

        Returns:
            str: The original, deobfuscated string.

        Usage:
            Use this method to retrieve sensitive information (e.g., passwords) that was previously
            obfuscated before storage. The obfuscation and deobfuscation routines must use the same
            number of rounds to ensure correct results.
        """
        string = base64.b64decode(string)

        for i in range(count):
            string = zlib.decompress(string)
            string = string.decode()
            string = list(string)
            string.reverse()
            string = "".join(string)
            string = base64.b64decode(string)

        # make unicode of bytes coming from base64 operations
        string = string.decode()

        return string

    def _default_actions(self):
        """
        Creates a set of default actions such as SSH, RDP, VNC, and others for use within the application.

        Returns:
            dict: A dictionary mapping action names to their corresponding Action objects.

        Usage:
            Called during initial configuration or when resetting to defaults to ensure a baseline set of actions
            is always available for the user.
        """
        if OS == OS_WINDOWS:
            defaultactions = {"RDP": Action(name="RDP", description="Connect via RDP.",
                                            type="command", string="C:/windows/system32/mstsc.exe /v:$ADDRESS$"),
                              "VNC": Action(name="VNC", description="Connect via VNC.",
                                            type="command", string="C:/Program Files/TightVNC/vncviewer.exe $ADDRESS$"),
                              "Telnet": Action(name="Telnet", description="Connect via Telnet.",
                                               type="command", string="C:/Windows/System32\telnet.exe root@$ADDRESS$"),
                              "SSH": Action(name="SSH", description="Connect via SSH.",
                                            type="command",
                                            string="C:/Program Files/PuTTY/putty.exe -l root $ADDRESS$")}
        elif OS == OS_MACOS:
            defaultactions = {"RDP": Action(name="RDP", description="Connect via RDP.",
                                            type="command", string="open rdp://$ADDRESS$"),
                              "VNC": Action(name="VNC", description="Connect via VNC.",
                                            type="command", string="open vnc://$ADDRESS$"),
                              "SSH": Action(name="SSH", description="Connect via SSH.",
                                            type="command", string="open ssh://root@$ADDRESS$"),
                              "Telnet": Action(name="Telnet", description="Connect via Telnet.",
                                               type="command", string="open telnet://root@$ADDRESS$")}
        else:
            # the Linux settings
            defaultactions = {"RDP": Action(name="RDP", description="Connect via RDP.",
                                            type="command", string="/usr/local/bin/rdesktop -g 1024x768 $ADDRESS$"),
                              "VNC": Action(name="VNC", description="Connect via VNC.",
                                            type="command", string="/usr/local/bin/vncviewer $ADDRESS$"),
                              "SSH": Action(name="SSH", description="Connect via SSH.",
                                            type="command", string="/usr/local/bin/gnome-terminal -x ssh root@$ADDRESS$"),
                              "Telnet": Action(name="Telnet", description="Connect via Telnet.",
                                               type="command",
                                               string="/usr/local/bin/gnome-terminal -x telnet root@$ADDRESS$"),
                              "Update-Linux": Action(name="Update-Linux", description="Run remote update script.",
                                                     type="command",
                                                     string="/usr/local/bin/terminator -x ssh root@$HOST$ update.sh",
                                                     enabled=False)}
        # OS agnostic actions as examples
        defaultactions["Nagios-1-Click-Acknowledge-Host"] = Action(name="Nagios-1-Click-Acknowledge-Host", type="url",
                                                                   description="Acknowledges a host with one click.",
                                                                   filter_target_service=False, enabled=False,
                                                                   string="$MONITOR-CGI$/cmd.cgi?cmd_typ=33&cmd_mod=2&host=$HOST$\
                        &com_author=$USERNAME$&com_data=acknowledged&btnSubmit=Commit")
        defaultactions["Nagios-1-Click-Acknowledge-Service"] = Action(name="Nagios-1-Click-Acknowledge-Service",
                                                                      type="url",
                                                                      description="Acknowledges a service with one click.",
                                                                      filter_target_host=False, enabled=False,
                                                                      string="$MONITOR-CGI$/cmd.cgi?cmd_typ=34&cmd_mod=2&host=$HOST$\
                        &service=$SERVICE$&com_author=$USERNAME$&com_data=acknowledged&btnSubmit=Commit")
        defaultactions["Opsview-Graph-Service"] = Action(name="Opsview-Graph-Service", type="browser",
                                                         description="Show graph in browser.", filter_target_host=False,
                                                         string="$MONITOR$/graph?service=$SERVICE$&host=$HOST$",
                                                         enabled=False)
        defaultactions["Opsview-History-Host"] = Action(name="Opsview-Host-Service", type="browser",
                                                        description="Show host in browser.", filter_target_host=True,
                                                        string="$MONITOR$/event?host=$HOST$", enabled=False)
        defaultactions["Opsview-History-Service"] = Action(name="Opsview-History-Service", type="browser",
                                                           description="Show history in browser.",
                                                           filter_target_host=True,
                                                           string="$MONITOR$/event?host=$HOST$&service=$SERVICE$",
                                                           enabled=False)
        defaultactions["Checkmk-1-Click-Acknowledge-Host"] = Action(name="Checkmk-1-Click-Acknowledge-Host",
                                                                    type="url",
                                                                    description="Acknowledges a host with one click.",
                                                                    filter_target_service=False, enabled=False,
                                                                    string="$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=hoststatus&host=$HOST$&_ack_comment=$COMMENT-ACK$&_acknowledge=Acknowledge")
        defaultactions["Checkmk-1-Click-Acknowledge-Service"] = Action(name="Checkmk-1-Click-Acknowledge-Service",
                                                                       type="url",
                                                                       description="Acknowledges a host with one click.",
                                                                       filter_target_host=False, enabled=False,
                                                                       string="$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=service&host=$HOST$&_ack_comment=$COMMENT-ACK$&_acknowledge=Acknowledge&service=$SERVICE$")
        defaultactions["Checkmk Edit host in WATO"] = Action(name="Checkmk Edit host in WATO", enabled=False,
                                                             monitor_type="Checkmk Multisite",
                                                             description="Edit host in WATO.",
                                                             string="$MONITOR$/wato.py?host=$HOST$&mode=edit_host")
        defaultactions["Email"] = Action(name="Email", enabled=False, description="Send email to someone.",
                                         type="browser",
                                         string="mailto:servicedesk@my.org?subject=Monitor alert: $HOST$ - $SERVICE$ - $STATUS-INFO$&body=Please help!.%0d%0aBest regards from Nagstamon")

        return defaultactions

    def _legacy_adjustments(self):
        """
        Performs legacy adjustments to configuration settings for improved clarity and future compatibility.

        This section updates configuration keys and values to reflect naming conventions and structural changes
        introduced in newer versions of the application. Specifically, it replaces any configuration keys or
        references containing 'nagios' with 'monitor' to standardize terminology. This is primarily a cosmetic
        change but helps maintain consistency and prepares the codebase for future enhancements.

        Special Considerations:
            - Ensures backward compatibility by migrating legacy configuration keys.
            - Only affects settings where 'nagios' is present, leaving other settings unchanged.
            - Intended to be clear and maintainable for future additions or refactoring.
        """
        for server in self.servers.values():
            if 'nagios_url' in server.__dict__.keys():
                server.monitor_url = server.nagios_url
            if 'nagios_cgi_url' in server.__dict__.keys():
                server.monitor_cgi_url = server.nagios_cgi_url

            # to reduce complexity in Centreon there is also only one URL necessary
            if server.type == "Centreon":
                server.monitor_url = server.monitor_cgi_url

        # switch to update interval in seconds not minutes
        if 'update_interval' in self.__dict__.keys():
            self.update_interval_seconds = int(self.update_interval) * 60
            self.__dict__.pop('update_interval')

        # remove support for GNOME2-trayicon-egg-stuff
        if 'statusbar_systray' in self.__dict__.keys():
            if self.statusbar_systray is True:
                self.icon_in_systray = True
            self.__dict__.pop('statusbar_systray')

        # some legacy action settings might need a little fix
        for action in self.actions.values():
            if not action.type.lower() in ('browser', 'command', 'url'):
                # set browser as default to make user notice something is wrong
                action.type = 'browser'

        # might be there only once after starting Nagstamon 3.4 the first time
        if 'save_config_after_urlencode' in self.__dict__.keys():
            self.save_config()

    def get_number_of_enabled_monitors(self):
        """
        Returns the number of enabled monitors.

        This method iterates through all configured servers and counts how many are currently enabled.
        If all monitors are disabled, there is no need to display the popup window in the application.

        Returns:
            int: The number of enabled monitor servers.

        Usage:
            Used to determine whether the popup window should be shown based on active monitors.
        """
        # to be returned
        number = 0
        for server in self.servers.values():
            # ##if str(server.enabled) == "True":
            if server.enabled is True:
                number += 1
        return number

    def delete_file(self, settings_dir, settings_file):
        """
            delete specified .conf file if setting is deleted in GUI
        """
        # clean up old deleted/renamed config file
        file = os.path.abspath('{1}{0}{2}{0}{3}'.format(os.sep, self.configdir, settings_dir, settings_file))
        if os.path.exists(file) and (os.path.isfile(file) or os.path.islink(file)):
            try:
                os.unlink(file)
            except Exception:
                import traceback
                traceback.print_exc(file=sys.stdout)


class Server:
    """
        one Server realized as object for config info
    """

    def __init__(self):
        self.enabled = True
        self.type = 'Nagios'
        self.name = 'Monitor server'
        self.monitor_url = 'https://monitor-server'
        self.monitor_cgi_url = 'https://monitor-server/monitor/cgi-bin'
        self.username = 'username'
        self.password = 'password'
        self.save_password = False
        self.use_proxy = False
        self.use_proxy_from_os = False
        self.proxy_address = 'http://proxyserver:port/'
        self.proxy_username = 'proxyusername'
        self.proxy_password = 'proxypassword'
        # defaults to 'basic', other possible values are 'digest' and 'kerberos'
        self.authentication = 'basic'
        self.timeout = 30
        # just GUI-wise deciding if more options are shown in server dialog
        self.show_options = False

        # SSL/TLS certificate verification
        self.ignore_cert = False
        self.custom_cert_use = False
        self.custom_cert_ca_file = ''

        self.idp_ecp_endpoint = 'https://idp/idp/profile/SAML2/SOAP/ECP'

        # special FX
        # Centreon autologin
        self.use_autologin = False
        self.autologin_key = ''

        # Icinga "host_display_name" instead of "host"
        self.use_display_name_host = False
        self.use_display_name_service = False

        # IcingaWeb2 might authenticate_credentials without cookies too - default is WITH cookies
        self.no_cookie_auth = False

        # Checkmk Multisite
        # Force Checkmk livestatus code to set AuthUser header for users who
        # are permitted to see all objects.
        self.force_authuser = False
        self.checkmk_view_hosts = 'nagstamon_hosts'
        self.checkmk_view_services = 'nagstamon_svc'

        # OP5 api filters
        self.host_filter = 'state !=0'
        self.service_filter = 'state !=0 or host.state != 0'

        # For more information about the Opsview options below, see this link:
        # https://knowledge.opsview.com/reference/api-status-filtering-service-objects

        # The Opsview hashtag filter will filter out any services NOT having the
        # listed hashtags (previously known as keywords).
        self.hashtag_filter = ''
        # The Opsview can_change_only option allows a user to show only
        # services for which the user has permissions to make changes OR set
        # downtimes.
        self.can_change_only = False

        # Sensu/Uchiwa/??? Datacenter/Site config
        self.monitor_site = 'Site 1'

        # Zabbix "Item Description" as "Service Name"
        self.use_description_name_service = False

        # Prometheus/Alertmanager mappings
        self.map_to_hostname = "pod_name,namespace,instance"
        self.map_to_servicename = "alertname"
        self.map_to_status_information = "message,summary,description"

        # Alertmanager mappings
        self.alertmanager_filter = ''
        self.map_to_critical = 'critical,error'
        self.map_to_warning = 'warning,warn'
        self.map_to_down = 'down'
        self.map_to_unknown = 'unknown'
        self.map_to_ok = 'ok'

        # IcingaDBWebNotificationsServer
        self.notification_filter = "user.name=*"
        self.notification_lookback = "30 minutes"

        # IcingaDBWeb custom filter
        self.custom_filter = ""

        # Thruk
        self.disabled_backends = ""

        # LibreNMS
        self.treat_services_as_alerts = False


class Action:
    """
    class for custom actions, which whill be thrown into one config dictionary like the servers
    """

    def __init__(self, **kwds):
        # to be or not to be enabled...
        self.enabled = True
        # monitor type
        self.monitor_type = ""
        # one of those: browser, url or command
        self.type = "browser"
        # thy name is...
        self.name = "Custom action"
        # OS of host where Nagstamon runs - especially commands are mostly not platform agnostic
        self.os = ""
        # description
        self.description = "Starts a custom action."
        # might be URL in case of type browser/url and a commandline for commands
        self.string = ""
        # version - maybe in future this might be more sophisticated
        self.version = "1"
        # kind of Nagios item this action is targeted to - maybe also usable for states
        self.filter_target_host = True
        self.filter_target_service = True
        # action applies only to certain hosts or services
        self.re_host_enabled = False
        self.re_host_pattern = ""
        self.re_host_reverse = False
        self.re_service_enabled = False
        self.re_service_pattern = ""
        self.re_service_reverse = False
        self.re_status_information_enabled = False
        self.re_status_information_pattern = ""
        self.re_status_information_reverse = False
        self.re_duration_enabled = False
        self.re_duration_pattern = ""
        self.re_duration_reverse = False
        self.re_attempt_enabled = False
        self.re_attempt_pattern = ""
        self.re_attempt_reverse = False
        self.re_groups_enabled = False
        self.re_groups_pattern = ""
        self.re_groups_reverse = False
        # close powin or not, depends on personal preference
        self.close_popwin = True
        self.leave_popwin_open = False
        # do an immediate recheck after action was applied
        self.recheck = False

        # special FX
        # Centreon criticality and autologin
        self.re_criticality_enabled = False
        self.re_criticality_pattern = ""
        self.re_criticality_reverse = False

        # add and/or all keywords to object
        for k in kwds:
            self.__dict__[k] = kwds[k]


# Initialize configuration to be accessed globally
conf = Config()

# try to get resources path if nagstamon got be installed by setup.py
RESOURCES = ""

try:
    # first try to find local resources directory in case Nagstamon was frozen with cx-Freeze for OSX or Windows
    executable_dir = os.path.join(os.sep.join(sys.executable.split(os.sep)[:-1]))
    if os.path.isdir(os.path.normcase(os.sep.join((executable_dir, "resources")))):
        RESOURCES = os.path.normcase(os.sep.join((executable_dir, "resources")))
    else:
        RESOURCES = str(resources.files("Nagstamon").joinpath("resources"))

except Exception as err:
    # get resources directory from current directory - only if not being set before
    # try-excepts necessary for platforms like Windows .EXE
    paths_to_check = [os.path.normcase(os.path.join(os.getcwd(), "Nagstamon", "resources")),
                      os.path.normcase(os.path.join(os.getcwd(), "resources"))]
    try:
        # if resources dir is not available in CWD, try the
        # libs dir (site-packages) for the current Python
        from distutils.sysconfig import get_python_lib

        paths_to_check.append(os.path.normcase(os.path.join(get_python_lib(), "Nagstamon", "resources")))
    except Exception:
        pass

    # if we're still out of luck, maybe this was a user scheme install
    try:
        import site

        site.getusersitepackages()  # make sure USER_SITE is set
        paths_to_check.append(os.path.normcase(os.path.join(site.USER_SITE, "Nagstamon", "resources")))
    except Exception:
        pass

    # add directory nagstamon.py where nagstamon.py resides for cases like 0install without installed pkg-resources
    paths_to_check.append(os.sep.join(sys.argv[0].split(os.sep)[:-1] + ["Nagstamon", "resources"]))

    for path in paths_to_check:
        if os.path.exists(path):
            RESOURCES = path
            break
    else:
        RESOURCES = str(Path(__file__).parent.absolute().joinpath('resources'))

# try to fix missing resources path for Windows
if OS == OS_WINDOWS:
    if r'\_internal\Nagstamon\resources' in RESOURCES:
        RESOURCES = RESOURCES.replace(r'\_internal\Nagstamon\resources', r'\_internal\resources')
