#!/usr/bin/python3
# TLP Profiles Daemon (tlp-pd)
# tlp-pd implements the D-Bus interface org.freedesktop.UPower.PowerProfile
# which lets desktop environments show a profile switch.
# TLP is used as the backend to apply these profiles.
#
# Copyright (c) 2026 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

import argparse  # noqa: I001
from gi.repository import Gio, GLib
import inspect
import logging
import logging.handlers
import os
import secrets
import signal
import string
import subprocess
import sys
from syslog import LOG_DAEMON
from typing import Dict, List, Optional
import warnings

# --- Constants
DAEMON_NAME = "TLP Profiles Daemon"
AVAILABLE_PROFILES = ["power-saver", "balanced", "performance"]
PROFILE_MAXLEN = 12
MAX_HOLDS = 16
COOKIE_ATTEMPTS = 10
MAX_UINT32 = 0xFFFFFFFF

# --- D-Bus constants
BUS_NAME = "org.freedesktop.UPower.PowerProfiles"
INTERFACE_NAME = BUS_NAME
OBJECT_PATH = "/org/freedesktop/UPower/PowerProfiles"
BUS_NAME_LEGACY = "net.hadess.PowerProfiles"
INTERFACE_NAME_LEGACY = BUS_NAME_LEGACY
OBJECT_PATH_LEGACY = "/net/hadess/PowerProfiles"
INTERFACE_PROPERTIES = "org.freedesktop.DBus.Properties"
NAME_MAXLEN = 255

# --- TLP constants
TLP_LAST_PROFILE = "/run/tlp/last_pwr"
TLP_PROFILE_IDX = ["2", "1", "0"]

# --- Introspection XML
INTROSPECTION_XML = """
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
  <interface name="org.freedesktop.UPower.PowerProfiles">
    <method name="HoldProfile">
      <arg type="s" name="profile" direction="in"/>
      <arg type="s" name="reason" direction="in"/>
      <arg type="s" name="application_id" direction="in"/>
      <arg type="u" name="cookie" direction="out"/>
    </method>
    <method name="ReleaseProfile">
      <arg type="u" name="cookie" direction="in"/>
    </method>
    <method name="SetActionEnabled">
      <arg type="s" name="action" direction="in"/>
      <arg type="b" name="enabled" direction="in"/>
    </method>
    <method name="SyncProfile">
      <arg type="s" name="profile" direction="in"/>
      <arg type="b" name="autosw" direction="in"/>
    </method>
    <signal name="ProfileReleased">
      <arg type="u" name="cookie"/>
    </signal>
    <property type="s" name="ActiveProfile" access="readwrite"/>
    <property type="s" name="PerformanceInhibited" access="read"/>
    <property type="s" name="PerformanceDegraded" access="read"/>
    <property type="aa{sv}" name="Profiles" access="read"/>
    <property type="as" name="Actions" access="read"/>
    <property type="aa{sv}" name="ActionsInfo" access="read"/>
    <property type="aa{sv}" name="ActiveProfileHolds" access="read"/>
    <property type="b" name="BatteryAware" access="readwrite"/>
    <property type="s" name="LogLevel" access="readwrite"/>
    <property type="s" name="Version" access="read"/>
  </interface>
</node>
"""

INTROSPECTION_XML_LEGACY = """
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
  <interface name="net.hadess.PowerProfiles">
    <method name="HoldProfile">
      <arg type="s" name="profile" direction="in"/>
      <arg type="s" name="reason" direction="in"/>
      <arg type="s" name="application_id" direction="in"/>
      <arg type="u" name="cookie" direction="out"/>
    </method>
    <method name="ReleaseProfile">
      <arg type="u" name="cookie" direction="in"/>
    </method>
    <signal name="ProfileReleased">
      <arg type="u" name="cookie"/>
    </signal>
    <property type="s" name="ActiveProfile" access="readwrite"/>
    <property type="s" name="PerformanceInhibited" access="read"/>
    <property type="s" name="PerformanceDegraded" access="read"/>
    <property type="aa{sv}" name="Profiles" access="read"/>
    <property type="as" name="Actions" access="read"/>
    <property type="aa{sv}" name="ActiveProfileHolds" access="read"/>
    <property type="s" name="Version" access="read"/>
  </interface>
</node>
"""


# --- D-Bus service implementing the power-profiles-daemon API
class ProfilesDaemon:
    def __init__(self, logger: logging.Logger, loglevel: str) -> None:
        # Initialize the profiles daemon
        self._logger = logger
        self._loglevel = loglevel
        self._ifaces_seen: set[str] = set()
        self._connection: Gio.DBusConnection = None
        self._registration_ids: list[int] = []
        self._owner_ids: list[int] = []

        # Internal state
        self._active_profile = self._get_tlp_profile()  # Last profile applied by TLP
        self._selected_profile = (
            self._active_profile
        )  # Last profile received by Set(ActiveProfile)
        self._profiles = [
            {
                "Profile": GLib.Variant("s", profile),
                "CpuDriver": GLib.Variant("s", "tlp"),
                "PlatformDriver": GLib.Variant("s", "tlp"),
            }
            for profile in AVAILABLE_PROFILES
        ]
        self._performance_degraded = ""
        self._actions: list[str] = []
        self._actions_info = []
        self._version = "tlp-pd 1.10.0"
        self._battery_aware = True
        self._holds = {}  # cookie -> {profile, reason, application_id, sender}

        # Parse introspection data
        self._node_info = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML)
        self._node_info_legacy = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML_LEGACY)

    def start(self) -> None:
        # Connect to system bus and register objects
        self._connection = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)

        ### FIXME: Ignore premature deprecation warnings for Gio.DBusConnection.register_object()
        ### FIXME: Replace with register_object_with-closure2() once GLib 2.84 is generally available
        warnings.filterwarnings("ignore", category=DeprecationWarning)

        # Register main interface
        self._registration_ids.append(
            self._connection.register_object(
                OBJECT_PATH,
                self._node_info.interfaces[0],
                self._handle_method_call,
                self._get_property,
                self._set_property,
            )
        )

        # Register legacy interface
        self._registration_ids.append(
            #
            self._connection.register_object(
                OBJECT_PATH_LEGACY,
                self._node_info_legacy.interfaces[0],
                self._handle_method_call,
                self._get_property,
                self._set_property,
            )
        )

        warnings.filterwarnings("default", category=DeprecationWarning)

        # Own bus names
        self._owner_ids.append(
            Gio.bus_own_name(
                Gio.BusType.SYSTEM,
                BUS_NAME,
                Gio.BusNameOwnerFlags.NONE,
                None,
                None,
                None,
            )
        )

        self._owner_ids.append(
            Gio.bus_own_name(
                Gio.BusType.SYSTEM,
                BUS_NAME_LEGACY,
                Gio.BusNameOwnerFlags.NONE,
                None,
                None,
                None,
            )
        )

    def _handle_method_call(
        self,
        connection: Gio.DBusConnection,
        sender: str,
        object_path: str,
        interface_name: str,
        method_name: str,
        parameters: GLib.Variant,
        invocation: Gio.DBusMethodInvocation,
    ) -> None:
        # Handle method calls on main or legacy interface
        _logtag = f"{_method_name()}"

        try:
            # Handle main and legacy interface methods
            if method_name == "HoldProfile":
                profile, reason, application_id = parameters.unpack()
                result = self._method_hold_profile(
                    profile, reason, application_id, sender, interface_name
                )
                invocation.return_value(GLib.Variant("(u)", (result,)))
            elif method_name == "ReleaseProfile":
                cookie = parameters.unpack()[0]
                self._method_release_profile(cookie, sender)
                invocation.return_value(None)
            elif method_name == "SetActionEnabled":
                action, enabled = parameters.unpack()
                self._method_set_action_enabled(action, enabled)
                invocation.return_value(None)
            elif method_name == "SyncProfile":
                profile, autosw = parameters.unpack()
                self._method_sync_profile(profile, autosw, sender)
                invocation.return_value(None)
            else:
                invocation.return_error_literal(
                    Gio.dbus_error_quark(),
                    Gio.DBusError.UNKNOWN_METHOD,
                    f"{_logtag}: Unknown method '{method_name}'",
                )
        except GLib.Error as e:
            self._logger.error(f"{_logtag}/{e.message}")
            invocation.return_error_literal(Gio.dbus_error_quark(), e.code, e.message)
        except Exception as e:
            self._logger.error(f"{_logtag}/{e.message}")
            invocation.return_error_literal(Gio.dbus_error_quark(), e.code, e.message)

    # --- Method implementations

    def _method_hold_profile(
        self,
        profile: str,
        reason: str,
        application_id: str,
        requester: str,
        req_iface: str,
    ) -> int:
        # Hold a new profile
        profile = _sanitize_input(profile, trunc_len=PROFILE_MAXLEN)
        reason = _sanitize_input(reason)
        application_id = _sanitize_input(application_id)
        # Note: requester and req_iface are already sanitized by _handle_method_call
        _logtag = f"{_method_name()}(profile='{profile}', reason='{reason}', appid='{application_id}', req='{requester}', iface='{req_iface}')"

        if profile not in AVAILABLE_PROFILES:
            raise GLib.Error(
                code=Gio.DBusError.INVALID_ARGS,
                message=f"{_logtag}: Invalid profile '{profile}'",
            )

        # Check authorization
        if not self._check_polkit_auth(requester, f"{BUS_NAME}.hold-profile"):
            raise GLib.Error(
                code=Gio.DBusError.AUTH_FAILED,
                message=f"{_logtag}: Not authorized to hold profile",
            )

        if len(self._holds) >= MAX_HOLDS:
            raise GLib.Error(
                code=Gio.DBusError.FAILED,
                message=f"{_logtag}: Maximum number of simultaneous holds ({MAX_HOLDS}) is used up",
            )

        # Create unique uint32 cookie
        for _ in range(1, COOKIE_ATTEMPTS):
            cookie = secrets.randbits(32)
            if cookie not in self._holds:
                self._holds[cookie] = {
                    "profile": profile,
                    "reason": reason,
                    "application_id": application_id,
                    "requester": requester,
                    "interface": req_iface,
                }
                break
        else:
            raise GLib.Error(
                code=Gio.DBusError.FAILED,
                message=f"{_logtag}: Failed to create random cookie after {COOKIE_ATTEMPTS} attempts",
            )

        self._logger.debug(f"{_logtag}: cookie: '{cookie}'")

        # Update active profile based on holds
        self._update_profile_from_holds()

        return cookie

    def _method_release_profile(self, cookie: int, sender: str) -> None:
        # Release a profile hold
        # Note: sender is already sanitized by _handle_method_call
        scookie = _sanitize_uint32(str(cookie))
        _logtag = f"{_method_name()}(scookie='{scookie}', sender='{sender}')"

        if scookie < 0:
            raise GLib.Error(
                code=Gio.DBusError.INVALID_ARGS,
                message=f"{_logtag}: Invalid cookie '{scookie}'",
            )

        # Check authorization
        if not self._check_polkit_auth(sender, f"{BUS_NAME}.release-profile"):
            raise GLib.Error(
                code=Gio.DBusError.AUTH_FAILED,
                message=f"{_logtag}: Not authorized to release profile",
            )

        if scookie not in self._holds:
            raise GLib.Error(
                code=Gio.DBusError.INVALID_ARGS,
                message=f"{_logtag}: Unknown cookie '{scookie}'",
            )

        hold_rel = self._holds.pop(scookie)
        self._logger.debug(
            f"{_logtag}: hold: '{hold_rel['profile']}', appid: '{hold_rel['application_id']}'"
        )

        # Update active profile based on remaining holds
        self._update_profile_from_holds()

    def _method_set_action_enabled(self, action: str, enabled: bool) -> None:
        # Set a particular action to be enabled or disabled
        _logtag = f"{_method_name()}()"
        action = _sanitize_input(action)

        # No actions supported
        raise GLib.Error(
            code=Gio.DBusError.NOT_SUPPORTED,
            message=f"{_logtag}: No such action '{action}'",
        )

    def _method_sync_profile(self, profile: str, autosw: bool, sender: str) -> None:
        # Callback for TLP after profile changes
        # Note: sender is already sanitized by _handle_method_call
        profile = _sanitize_input(profile, trunc_len=PROFILE_MAXLEN)
        _logtag = f"{_method_name()}(profile='{profile}', sender='{sender}')"

        if profile not in AVAILABLE_PROFILES:
            self._logger.error(f"{_logtag}: Invalid profile '{profile}'")
            return

        if not self._check_polkit_auth(sender, f"{BUS_NAME}.sync-profile"):
            raise GLib.Error(
                code=Gio.DBusError.AUTH_FAILED,
                message=f"{_logtag}: Not authorized to sync profile",
            )

        # Update active profile and battery aware flag
        self._active_profile = profile
        self._battery_aware = autosw
        self._logger.debug(f"{_logtag}")

        # Signal the desktop about the new profile
        self._signal_properties_changed(
            {"ActiveProfile": GLib.Variant("s", profile)}, []
        )

    # --- Property handlers

    def _get_property(
        self,
        connection: Gio.DBusConnection,
        sender: str,
        object_path: str,
        interface_name: str,
        property_name: str,
    ) -> Optional[GLib.Variant]:
        # Handle Get() through the main or legacy interface
        _logtag = f"{_method_name()}(interface='{interface_name}', property='{property_name}', sender='{sender}')"

        # Remember receiving interface for later use with PropertiesChanged signal
        self._ifaces_seen.add(interface_name)

        if property_name == "ActiveProfile":
            retval = GLib.Variant("s", self._active_profile)
        elif property_name == "PerformanceInhibited":
            retval = GLib.Variant("s", "")  # Deprecated
        elif property_name == "PerformanceDegraded":
            retval = GLib.Variant("s", self._performance_degraded)
        elif property_name == "Profiles":
            retval = GLib.Variant("aa{sv}", self._profiles)
        elif property_name == "Actions":
            retval = GLib.Variant("as", self._actions)
        elif property_name == "ActionsInfo":
            retval = GLib.Variant("aa{sv}", self._actions_info)
        elif property_name == "ActiveProfileHolds":
            retval = self._get_active_profile_holds()
        elif property_name == "Version":
            retval = GLib.Variant("s", self._version)
        elif property_name == "BatteryAware":
            retval = GLib.Variant("b", self._battery_aware)
        elif property_name == "LogLevel":
            retval = GLib.Variant("s", self._loglevel)
        else:
            raise GLib.Error(
                code=Gio.DBusError.UNKNOWN_PROPERTY,
                message=f"{_logtag}: Unknown property '{property_name}'",
            )

        self._logger.debug(f"{_logtag}: {retval}")
        return retval

    def _set_property(
        self,
        connection: Gio.DBusConnection,
        sender: str,
        object_path: str,
        interface_name: str,
        property_name: str,
        value: GLib.Variant,
    ) -> bool:
        # Handle Set() through the main or legacy interface
        _logtag = f"{_method_name()}(interface='{interface_name}', property='{property_name}', value={value}, sender='{sender}')"

        # Remember receiving interface for later use with PropertiesChanged signal
        self._ifaces_seen.add(interface_name)

        if property_name == "ActiveProfile":
            profile = _sanitize_input(value.get_string(), trunc_len=PROFILE_MAXLEN)

            # Check authorization
            if not self._check_polkit_auth(sender, f"{BUS_NAME}.switch-profile"):
                self._logger.error(f"{_logtag}: Not authorized to switch profile")
                return False

            # User (manually) changes profile
            self._drop_all_holds()
            if not self._apply_profile_with_tlp(profile):
                return False

            self._selected_profile = profile
            # Note: ActiveProfile is signaled in _method_sync_profile() after TLP applied the profile

        elif property_name == "BatteryAware":
            # BatteryAware is determined internally by TLP_AUTO_SWITCH and therefore
            # read-only via the D-Bus property API
            # Silently discard property change so as not to startle other API users
            self._logger.error(
                f"{_logtag}: Property BatteryAware can only be changed via TLP_AUTO_SWITCH"
            )
            return True

        elif property_name == "LogLevel":
            # Check authorization
            if not self._check_polkit_auth(sender, f"{BUS_NAME}.set-loglevel"):
                self._logger.error(f"{_logtag}: Not authorized to set loglevel")
                return False

            loglevel = _sanitize_input(value.get_string(), trunc_len=5)
            if loglevel == "debug":
                self._logger.setLevel(logging.DEBUG)
                self._loglevel = "debug"
            else:
                self._logger.setLevel(logging.INFO)
                self._loglevel = "info"

            self._signal_properties_changed({property_name: value}, [])

        else:
            raise GLib.Error(
                code=Gio.DBusError.UNKNOWN_PROPERTY,
                message=f"{_logtag}: Property '{property_name}' is readonly or unknown",
            )

        self._logger.debug(f"{_logtag}: ok")

        return True

    def _signal_properties_changed(
        self,
        changed_properties: Dict[str, GLib.Variant],
        invalidated: List[str] = [],
    ) -> None:
        # Emit PropertiesChanged signal on noted interfaces
        _logtag = f"{_method_name()}(changed_properties={changed_properties}, invalidated={invalidated})"

        for iface in self._ifaces_seen:
            # Determine which object path to use
            if iface == INTERFACE_NAME_LEGACY:
                object_path = OBJECT_PATH_LEGACY
            else:
                object_path = OBJECT_PATH

            if not self._connection.emit_signal(
                None,  # destination
                object_path,
                INTERFACE_PROPERTIES,
                "PropertiesChanged",
                GLib.Variant("(sa{sv}as)", (iface, changed_properties, invalidated)),
            ):
                self._logger.warning(
                    f"{_logtag}: Failed to emit PropertiesChanged signal on {iface}"
                )
            else:
                self._logger.debug(
                    f"{_logtag}: PropertiesChanged signal emitted on {iface}"
                )

    def _get_active_profile_holds(self) -> GLib.Variant:
        # Get list of active profile holds
        active_holds = []
        for cookie, hold_info in self._holds.items():
            active_holds.append(
                {
                    "ApplicationId": GLib.Variant("s", hold_info["application_id"]),
                    "Profile": GLib.Variant("s", hold_info["profile"]),
                    "Reason": GLib.Variant("s", hold_info["reason"]),
                }
            )
        return GLib.Variant("aa{sv}", active_holds)

    # --- TLP helpers

    def _apply_profile_with_tlp(self, profile: str) -> bool:
        # Call TLP to apply the new profile
        _logtag = f"{_method_name()}()"

        if profile not in AVAILABLE_PROFILES:
            self._logger.error(f"{_logtag}: Invalid profile '{profile}'")
            return False

        self._logger.info(f"Run detached TLP to apply profile '{profile}'")
        _ = subprocess.Popen(
            ["tlp", f"{profile}"],
            start_new_session=True,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            close_fds=True,
        )
        return True

    def _get_tlp_profile(self) -> str:
        # Read TLP statefile and translate into the last profile applied
        _logtag = f"{_method_name()}()"

        try:
            with open(TLP_LAST_PROFILE, "r") as f:
                last = f.readline().strip().split()
                last_pp = last[0] if len(last) > 0 else ""
        except (FileNotFoundError, IOError) as errmsg:
            self._logger.info(
                f"{_logtag}: {errmsg} --> TLP may not have applied a profile yet."
            )
            return "balanced"

        try:
            idx = TLP_PROFILE_IDX.index(last_pp)

        except ValueError:
            self._logger.warning(f"{_logtag}: Unknown TLP profile '{last_pp}'")
            return "balanced"

        tlp_profile = AVAILABLE_PROFILES[idx]
        self._logger.debug(f"{_logtag}: Current TLP profile is '{tlp_profile}'")

        return tlp_profile

    # --- PolicyKit helper

    def _check_polkit_auth(self, sender: str, action_id: str) -> bool:
        # Check if the D-Bus sender is authorized for the PolicyKit action_id
        _logtag = f"{_method_name()}(sender='{sender}', action_id='{action_id}')"

        try:
            # Get proxy to PolicyKit authority
            proxy = Gio.DBusProxy.new_sync(
                self._connection,
                Gio.DBusProxyFlags.NONE,
                None,
                "org.freedesktop.PolicyKit1",
                "/org/freedesktop/PolicyKit1/Authority",
                "org.freedesktop.PolicyKit1.Authority",
                None,
            )

            # Call PolicyKit CheckAuthorization()
            result = proxy.call_sync(
                "CheckAuthorization",
                GLib.Variant(
                    "((sa{sv})sa{ss}us)",
                    (
                        ("system-bus-name", {"name": GLib.Variant("s", sender)}),
                        action_id,
                        {},  # No details
                        0x1,  # AllowUserInteraction flag
                        "",  # No cancellation_id
                    ),
                ),
                Gio.DBusCallFlags.NONE,
                -1,
                None,
            )

            # Extract is_authorized from result
            is_authorized = result.unpack()[0][0]
            self._logger.debug(f"{_logtag}: {is_authorized} {result}")
            return bool(is_authorized)

        except GLib.Error as e:
            self._logger.error(f"{_logtag}: PolicyKit error: {e}")
            return False

    def _update_profile_from_holds(self) -> None:
        # Change active profile to the last hold or revert to user selection
        if len(self._holds) == 0:
            self._logger.info(
                f"Changing active profile: from hold '{self._active_profile}' to last user selection '{self._selected_profile}'"
            )
            self._apply_profile_with_tlp(self._selected_profile)
        else:
            # Apply profile from the last hold
            last_cookie_key = list(self._holds)[-1]
            new_profile = self._holds[last_cookie_key]["profile"]
            new_appid = self._holds[last_cookie_key]["application_id"]
            old_profile = self._active_profile
            if new_profile != old_profile:
                self._logger.info(
                    f"Changing active profile: from hold '{old_profile}' to '{new_profile}' (appid: '{new_appid}')'"
                )
                self._apply_profile_with_tlp(new_profile)
            else:
                self._logger.info(
                    f"Keeping active profile: hold '{new_profile}' (appid: '{new_appid}')"
                )

    def _drop_all_holds(self) -> None:
        # Release all holds and notify holders
        _logtag = f"{_method_name()}()"

        self._logger.debug(f"{_logtag}")

        cookies2release = list(self._holds.keys())
        for cookie in cookies2release:
            hold2release = self._holds.pop(cookie)
            self._logger.info(
                f"Auto-releasing hold: profile '{hold2release['profile']}' (appid: '{hold2release['application_id']}')'"
            )

            if hold2release["interface"] == INTERFACE_NAME_LEGACY:
                object_path = OBJECT_PATH_LEGACY
            else:
                object_path = OBJECT_PATH

            # Emit ProfileReleased signal
            if not self._connection.emit_signal(
                hold2release["requester"],
                object_path,
                hold2release["interface"],
                "ProfileReleased",
                GLib.Variant("(u)", (cookie,)),
            ):
                self._logger.warning(
                    f"{_logtag}: Failed to emit ProfileReleased signal to {hold2release['requester']} on {hold2release['interface']}"
                )
            else:
                self._logger.debug(
                    f"{_logtag}: PropertiesChanged signal emitted to {hold2release['requester']} on {hold2release['interface']}"
                )


# --- Helper functions


def _method_name() -> str:
    # Return method name of caller
    return inspect.currentframe().f_back.f_code.co_name  # type: ignore[reportOptionalMemberAccess]


# --- Input sanitation

INPUT_ALLOWED_CHARS = (
    string.ascii_letters + string.digits + " !@'+-.,/:;_$&*()%<=>?#[]{|}^~" + '"'
)


def _sanitize_input(
    input_str: str, sani_ch: str = "§", trunc_len: int = NAME_MAXLEN
) -> str:
    # Replace any character not in INPUT_ALLOWED_CHARS with sani_char
    # Truncate input to trunc_len chars (default is 255, the max length of D-Bus names)
    input_str = input_str[:trunc_len]
    result_str = ""
    for ch in input_str:
        if ch in INPUT_ALLOWED_CHARS:
            result_str += ch
        else:
            result_str += sani_ch
    return result_str


def _sanitize_uint32(input: str) -> int:
    if not input.isdigit():
        return -1

    input_int = int(input)
    if input_int > MAX_UINT32:
        return -1

    return input_int


# --- MAIN


def main() -> None:
    # Parse arguments
    parser = argparse.ArgumentParser(description=f"{DAEMON_NAME}")
    _ = parser.add_argument(
        "--debug", "-D", action="store_true", help="log debugging messages"
    )
    args = parser.parse_args()

    # Check root privileges
    if os.geteuid() != 0:
        print(
            f"Error: root privileges are required to run the {DAEMON_NAME}.",
            file=sys.stderr,
        )
        sys.exit(1)

    # Automatically reap tlp zombie subprocesses
    signal.signal(signal.SIGCHLD, signal.SIG_IGN)

    # --- Set up logging
    logger = logging.getLogger(__name__)
    handler = logging.handlers.SysLogHandler(address="/dev/log", facility=LOG_DAEMON)
    logger.addHandler(handler)

    # Set syslog identifier "tlp-pd"
    formatter = logging.Formatter(f"{os.path.basename(__file__)}: %(message)s")
    handler.setFormatter(formatter)

    # Set syslog level
    if args.debug:
        logger.setLevel(logging.DEBUG)
        loglevel = "debug"
    else:
        logger.setLevel(logging.INFO)
        loglevel = "info"

    # --- Create daemon instance
    daemon = ProfilesDaemon(logger, loglevel)
    daemon.start()

    logger.info(f"{DAEMON_NAME} started.")
    logger.info(f"Interfaces: {BUS_NAME}, {BUS_NAME_LEGACY}")
    logger.info(f"Object path: {OBJECT_PATH}")
    logger.info(f"Arguments: {vars(args)}")
    logger.info(f"Loglevel: {loglevel}")
    logger.info(f"Initial profile: {daemon._active_profile}")

    # --- Run main loop
    try:
        mainloop = GLib.MainLoop()
        mainloop.run()
    except KeyboardInterrupt:
        logger.info(f"{DAEMON_NAME} shutting down ...")


if __name__ == "__main__":
    main()
