#!/usr/bin/env python3
"""
syncwhen - Conditionally control syncthing.service based on system state
"""

import dbus
import gi
import os
import sys
import tomllib
from abc import ABC, abstractmethod
from dbus.mainloop.glib import DBusGMainLoop
from pathlib import Path
from typing import Optional

gi.require_version("Notify", "0.7")
from gi.repository import GLib, Notify


class Condition(ABC):
    """Base class for conditions that control syncthing"""

    def __init__(self, config: dict, system_bus: dbus.Bus, session_bus: dbus.Bus):
        self.config = config
        self.system_bus = system_bus
        self.session_bus = session_bus
        self.available = True

    @abstractmethod
    def is_enabled(self) -> bool:
        """Check if this condition is enabled in config"""
        pass

    @abstractmethod
    def is_satisfied(self) -> bool:
        """Check if current system state satisfies this condition"""
        pass

    @abstractmethod
    def get_name(self) -> str:
        """Return condition name for logging"""
        pass

    @abstractmethod
    def setup(self, on_change_callback):
        """Subscribe to D-Bus signals and set up monitoring"""
        pass

    @abstractmethod
    def get_current_state(self) -> str:
        """Return human-readable description of current state"""
        pass


class ACPowerCondition(Condition):
    """Condition: System is on AC power"""

    def __init__(self, config: dict, system_bus: dbus.Bus, session_bus: dbus.Bus):
        super().__init__(config, system_bus, session_bus)
        try:
            self.upower = system_bus.get_object(
                "org.freedesktop.UPower", "/org/freedesktop/UPower"
            )
            self.upower_props = dbus.Interface(
                self.upower, "org.freedesktop.DBus.Properties"
            )
        except dbus.DBusException as e:
            print(f"Warning: UPower not available: {e}")
            self.available = False

    def is_enabled(self) -> bool:
        return self.config.get("conditions", {}).get("ac_power", False)

    def is_satisfied(self) -> bool:
        if not self.available:
            return False
        try:
            on_battery = self.upower_props.Get("org.freedesktop.UPower", "OnBattery")
            return not on_battery  # Satisfied when NOT on battery (i.e., on AC)
        except dbus.DBusException as e:
            print(f"Error reading AC power state: {e}")
            return False

    def get_name(self) -> str:
        return "ac_power"

    def setup(self, on_change_callback):
        if not self.available:
            return

        def properties_changed(interface, changed_props, invalidated_props):
            if "OnBattery" in changed_props:
                on_battery = changed_props["OnBattery"]
                print(
                    f"AC power changed: {'disconnected' if on_battery else 'connected'}"
                )
                on_change_callback()

        self.upower_props.connect_to_signal("PropertiesChanged", properties_changed)

    def get_current_state(self) -> str:
        if not self.available:
            return "unavailable"
        try:
            on_battery = self.upower_props.Get("org.freedesktop.UPower", "OnBattery")
            return "on battery" if on_battery else "on AC power"
        except dbus.DBusException:
            return "unknown"


class WiFiWhitelistCondition(Condition):
    """Condition: Connected to whitelisted WiFi SSID"""

    def __init__(self, config: dict, system_bus: dbus.Bus, session_bus: dbus.Bus):
        super().__init__(config, system_bus, session_bus)
        try:
            self.nm = system_bus.get_object(
                "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager"
            )
            self.nm_props = dbus.Interface(self.nm, "org.freedesktop.DBus.Properties")
        except dbus.DBusException as e:
            print(f"Warning: NetworkManager not available: {e}")
            self.available = False

    def is_enabled(self) -> bool:
        return self.config.get("conditions", {}).get("wifi_whitelist", False)

    def _get_current_ssid(self) -> Optional[str]:
        """Get currently connected WiFi SSID, if any"""
        if not self.available:
            return None

        try:
            # Get active connections
            active_connections = self.nm_props.Get(
                "org.freedesktop.NetworkManager", "ActiveConnections"
            )

            for conn_path in active_connections:
                conn = self.system_bus.get_object(
                    "org.freedesktop.NetworkManager", conn_path
                )
                conn_props = dbus.Interface(conn, "org.freedesktop.DBus.Properties")

                # Get devices for this connection
                devices = conn_props.Get(
                    "org.freedesktop.NetworkManager.Connection.Active", "Devices"
                )

                for device_path in devices:
                    device = self.system_bus.get_object(
                        "org.freedesktop.NetworkManager", device_path
                    )
                    device_props = dbus.Interface(
                        device, "org.freedesktop.DBus.Properties"
                    )

                    # Check if it's a WiFi device (DeviceType 2)
                    device_type = device_props.Get(
                        "org.freedesktop.NetworkManager.Device", "DeviceType"
                    )
                    if device_type != 2:
                        continue

                    # Get active access point
                    ap_path = device_props.Get(
                        "org.freedesktop.NetworkManager.Device.Wireless",
                        "ActiveAccessPoint",
                    )
                    if ap_path == "/":
                        continue

                    # Get SSID from access point
                    ap = self.system_bus.get_object(
                        "org.freedesktop.NetworkManager", ap_path
                    )
                    ap_props = dbus.Interface(ap, "org.freedesktop.DBus.Properties")
                    ssid_bytes = ap_props.Get(
                        "org.freedesktop.NetworkManager.AccessPoint", "Ssid"
                    )

                    # Decode SSID from bytes to string
                    ssid = bytes(ssid_bytes).decode("utf-8", errors="replace")
                    return ssid

            return None
        except dbus.DBusException as e:
            print(f"Error reading WiFi state: {e}")
            return None

    def is_satisfied(self) -> bool:
        if not self.available:
            return False

        current_ssid = self._get_current_ssid()
        if current_ssid is None:
            return False

        whitelist = self.config.get("wifi_whitelist", {}).get("ssids", [])
        return current_ssid in whitelist

    def get_name(self) -> str:
        return "wifi_whitelist"

    def setup(self, on_change_callback):
        if not self.available:
            return

        def state_changed(*args):
            print("Network state changed")
            on_change_callback()

        self.nm_props.connect_to_signal("PropertiesChanged", state_changed)

    def get_current_state(self) -> str:
        if not self.available:
            return "unavailable"
        ssid = self._get_current_ssid()
        return f"connected to '{ssid}'" if ssid else "not connected to WiFi"


class PowerProfileCondition(Condition):
    """Condition: System power profile matches allowed profiles"""

    def __init__(self, config: dict, system_bus: dbus.Bus, session_bus: dbus.Bus):
        super().__init__(config, system_bus, session_bus)
        try:
            self.ppd = system_bus.get_object(
                "org.freedesktop.UPower.PowerProfiles",
                "/org/freedesktop/UPower/PowerProfiles",
            )
            self.ppd_props = dbus.Interface(self.ppd, "org.freedesktop.DBus.Properties")
        except dbus.DBusException as e:
            print(f"Warning: power-profiles-daemon not available: {e}")
            self.available = False

    def is_enabled(self) -> bool:
        return self.config.get("conditions", {}).get("power_profiles", False)

    def is_satisfied(self) -> bool:
        if not self.available:
            return False

        try:
            current_profile = self.ppd_props.Get(
                "org.freedesktop.UPower.PowerProfiles", "ActiveProfile"
            )
            profile_config = self.config.get("power_profiles", {})
            return profile_config.get(current_profile, False)
        except dbus.DBusException as e:
            print(f"Error reading power profile: {e}")
            return False

    def get_name(self) -> str:
        return "power_profiles"

    def setup(self, on_change_callback):
        if not self.available:
            return

        def properties_changed(interface, changed_props, invalidated_props):
            if "ActiveProfile" in changed_props:
                profile = changed_props["ActiveProfile"]
                print(f"Power profile changed to: {profile}")
                on_change_callback()

        self.ppd_props.connect_to_signal("PropertiesChanged", properties_changed)

    def get_current_state(self) -> str:
        if not self.available:
            return "unavailable"
        try:
            profile = self.ppd_props.Get(
                "org.freedesktop.UPower.PowerProfiles", "ActiveProfile"
            )
            return f"profile={profile}"
        except dbus.DBusException:
            return "unknown"


class SyncthingController:
    """Main daemon controller"""

    def __init__(self, config_path: Path):
        self.config_path = config_path
        self.config = self._load_config()
        self.override_active = False
        self.notification = None

        # Initialize D-Bus
        DBusGMainLoop(set_as_default=True)
        self.system_bus = dbus.SystemBus()
        self.session_bus = dbus.SessionBus()

        # Initialize notifications
        Notify.init("syncwhen")

        # Initialize conditions
        self.conditions = [
            ACPowerCondition(self.config, self.system_bus, self.session_bus),
            WiFiWhitelistCondition(self.config, self.system_bus, self.session_bus),
            PowerProfileCondition(self.config, self.system_bus, self.session_bus),
        ]

    def _load_config(self) -> dict:
        """Load configuration from TOML file"""
        try:
            with open(self.config_path, "rb") as f:
                return tomllib.load(f)
        except FileNotFoundError:
            print(f"Error: Config file not found: {self.config_path}")
            sys.exit(1)
        except tomllib.TOMLDecodeError as e:
            print(f"Error: Invalid TOML config: {e}")
            sys.exit(1)


    def _get_systemd_manager(self) -> dbus.Interface:
        systemd = self.session_bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
        return dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')

    def _get_syncthing_state(self) -> bool:
        """Query if syncthing.service is currently active"""
        try:
            manager = self._get_systemd_manager()
            unit_path = manager.GetUnit('syncthing.service')

            # query ActiveState property directly
            unit_obj = self.session_bus.get_object('org.freedesktop.systemd1', unit_path)
            unit_props = dbus.Interface(unit_obj, 'org.freedesktop.DBus.Properties')
            active_state = unit_props.Get('org.freedesktop.systemd1.Unit', 'ActiveState')
            return active_state == 'active'
        except dbus.DBusException as e:
            # unit might not be loaded yet, which is fine
            if 'NoSuchUnit' in str(e):
                return False
            print(f"Warning: Cannot query syncthing.service state: {e}")
            return False

    def _start_syncthing(self):
        """Start syncthing.service"""
        try:
            print("Starting syncthing.service")
            manager = self._get_systemd_manager()
            manager.StartUnit("syncthing.service", "replace")
        except dbus.DBusException as e:
            print(f"Error starting syncthing: {e}")

    def _stop_syncthing(self):
        """Stop syncthing.service"""
        try:
            print("Stopping syncthing.service")
            manager = self._get_systemd_manager()
            manager.StopUnit("syncthing.service", "replace")
        except dbus.DBusException as e:
            print(f"Error stopping syncthing: {e}")

    def _evaluate_conditions(self) -> bool:
        """
        Evaluate all conditions with AND logic.
        Returns True if syncthing should run.
        """
        enabled_conditions = [c for c in self.conditions if c.is_enabled()]

        # If no conditions enabled, syncthing always runs
        if not enabled_conditions:
            print("No conditions enabled - syncthing unrestricted")
            return True

        # Evaluate each enabled condition - ALL must be satisfied
        satisfied = []
        failed = []

        for condition in enabled_conditions:
            if condition.is_satisfied():
                satisfied.append(condition.get_name())
            else:
                failed.append(
                    f"{condition.get_name()} ({condition.get_current_state()})"
                )

        # AND logic: all enabled conditions must be satisfied
        if failed:
            print(f"Conditions NOT satisfied - failed: {', '.join(failed)}")
            return False
        else:
            print(f"All conditions satisfied: {', '.join(satisfied)}")
            return True

    def _show_notification(
        self, title: str, body: str, action_label: Optional[str] = None
    ):
        """Show a notification, optionally with an action button"""
        if self.notification:
            self.notification.close()

        notification = Notify.Notification.new(title, body, None)
        notification.set_urgency(Notify.Urgency.NORMAL)

        if action_label:
            notification.add_action(
                "toggle", action_label, self._on_notification_action, None
            )

        notification.show()
        self.notification = notification

    def _on_notification_action(self, notification, action, user_data):
        """Handle notification action button click"""
        print("User clicked notification action - toggling override")
        self.override_active = True

        current_state = self._get_syncthing_state()

        if current_state:
            self._stop_syncthing()
            # Wait a moment then show override notification
            GLib.timeout_add(
                500,
                lambda: self._show_notification(
                    "Syncthing Override",
                    "Syncthing stopped manually. Override active until conditions change.",
                    None,
                ),
            )
        else:
            self._start_syncthing()
            # Wait a moment then show override notification
            GLib.timeout_add(
                500,
                lambda: self._show_notification(
                    "Syncthing Override",
                    "Syncthing started manually. Override active until conditions change.",
                    None,
                ),
            )

    def _on_condition_change(self):
        """Handle condition state change"""
        if self.override_active:
            print("Condition changed - clearing override")
            self.override_active = False

        self._evaluate_and_act()

    def _evaluate_and_act(self):
        """Evaluate conditions and start/stop syncthing if needed"""
        should_run = self._evaluate_conditions()
        current_state = self._get_syncthing_state()

        if should_run and not current_state:
            self._start_syncthing()
            self._show_notification(
                "Syncthing Started",
                "Syncthing is now running based on current conditions.",
                "Stop anyway",
            )
        elif not should_run and current_state:
            self._stop_syncthing()
            self._show_notification(
                "Syncthing Stopped",
                "Syncthing stopped due to current conditions.",
                "Start anyway",
            )

    def start(self):
        """Start the daemon"""
        print("=== Syncthing Daemon Control Starting ===")
        print(f"Config: {self.config_path}")

        # Log initial state
        print("\n=== Initial State ===")
        for condition in self.conditions:
            if condition.available:
                print(
                    f"{condition.get_name()}: enabled={condition.is_enabled()}, {condition.get_current_state()}"
                )
            else:
                print(f"{condition.get_name()}: unavailable")

        current_state = self._get_syncthing_state()
        print(f"syncthing.service: {'active' if current_state else 'inactive'}")

        # Set up condition monitoring
        print("\n=== Setting up monitoring ===")
        for condition in self.conditions:
            condition.setup(self._on_condition_change)

        # Initial evaluation
        print("\n=== Initial Evaluation ===")
        self._evaluate_and_act()

        # Run main loop
        print("\n=== Monitoring started ===")
        loop = GLib.MainLoop()
        try:
            loop.run()
        except KeyboardInterrupt:
            print("\nShutting down...")
            loop.quit()


def main():
    # Determine config path
    config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
    config_path = Path(config_home) / "syncwhen" / "config.toml"

    controller = SyncthingController(config_path)
    controller.start()


if __name__ == "__main__":
    main()
