use crate::app::Message;
use crate::i18n::{UnitSystem, unit_system};
use crate::services::upower::PeripheralDeviceKind;
use crate::utils::celsius_to_fahrenheit;
use hex_color::HexColor;
use iced::futures::StreamExt;
use iced::{Color, Subscription, futures::SinkExt, stream::channel, theme::palette};
use inotify::EventMask;
use inotify::Inotify;
use inotify::WatchMask;
use log::{debug, error, info, warn};
use regex::Regex;
use serde::{
    Deserialize, Deserializer,
    de::{self, MapAccess, Visitor},
};
use serde_with::DisplayFromStr;
use serde_with::serde_as;
use std::path::PathBuf;
use std::time::Duration;
use std::{collections::HashMap, error::Error, ops::Deref, path::Path};
use tokio::time::sleep;

pub const DEFAULT_CONFIG_FILE_PATH: &str = "~/.config/ashell/config.toml";

#[derive(Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "lowercase")]
pub enum LogTarget {
    #[default]
    File,
    Stdout,
    Stderr,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct LoggingConfig {
    pub level: String,
    pub target: LogTarget,
    pub directory: Option<PathBuf>,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            level: "warn".to_owned(),
            target: LogTarget::default(),
            directory: None,
        }
    }
}

impl LoggingConfig {
    pub fn log_directory(&self) -> PathBuf {
        let default_directory = || {
            crate::xdg::get_runtime_dir().unwrap_or_else(|| {
                [std::env::temp_dir(), PathBuf::from("ashell")]
                    .iter()
                    .collect()
            })
        };

        match &self.directory {
            Some(directory) => expand_path(directory.clone()).unwrap_or_else(|e| {
                eprintln!(
                    "ashell: warning: cannot expand logging.directory {directory:?}: {e}, using the default"
                );
                default_directory()
            }),
            None => default_directory(),
        }
    }
}

#[derive(Deserialize, Clone, Debug, Default)]
#[serde(default)]
pub struct Config {
    pub logging: LoggingConfig,
    pub language: Option<String>,
    pub region: Option<String>,
    pub position: Position,
    pub layer: Layer,
    pub outputs: Outputs,
    pub modules: Modules,
    #[serde(rename = "CustomModule")]
    pub custom_modules: Vec<CustomModuleDef>,
    pub updates: Option<UpdatesModuleConfig>,
    pub workspaces: WorkspacesModuleConfig,
    pub window_title: WindowTitleConfig,
    pub system_info: SystemInfoModuleConfig,
    pub notifications: NotificationsModuleConfig,
    pub tray: TrayModuleConfig,
    pub tempo: TempoModuleConfig,
    pub settings: SettingsModuleConfig,
    pub appearance: Appearance,
    pub media_player: MediaPlayerModuleConfig,
    pub keyboard_layout: KeyboardLayoutModuleConfig,
    pub animations: AnimationsConfig,
    pub enable_esc_key: bool,
    pub osd: OsdConfig,
}

#[derive(Deserialize, Clone, Debug, Default)]
#[serde(default)]
pub struct AnimationsConfig {
    pub enabled: bool,
}

impl Config {
    fn validate(&mut self) {
        if let Some(ref mut updates) = self.updates {
            updates.validate();
        }
        self.system_info.validate();
        self.tempo.validate();
        self.settings.validate();
        self.media_player.validate();
    }
}

#[derive(Deserialize, Clone, Debug)]
pub struct UpdatesModuleConfig {
    pub check_cmd: String,
    pub update_cmd: String,
    #[serde(default = "UpdatesModuleConfig::default_interval")]
    pub interval: u64,
}

impl UpdatesModuleConfig {
    const fn default_interval() -> u64 {
        3600
    }

    fn validate(&mut self) {
        if self.interval == 0 {
            warn!("UpdatesModuleConfig.interval is 0, setting to 1");
            self.interval = 1;
        }
    }
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum WorkspaceVisibilityMode {
    #[default]
    All,
    MonitorSpecific,
    MonitorSpecificExclusive,
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum WorkspaceIndicatorFormat {
    #[default]
    Name,
    NameAndIcons,
}

#[derive(Deserialize, Clone, Default, Debug)]
#[serde(default)]
pub struct WorkspacesModuleConfig {
    pub visibility_mode: WorkspaceVisibilityMode,
    pub indicator_format: WorkspaceIndicatorFormat,
    pub group_by_monitor: bool,
    pub enable_workspace_filling: bool,
    pub disable_special_workspaces: bool,
    pub max_workspaces: Option<u32>,
    pub workspace_names: Vec<String>,
    pub enable_virtual_desktops: bool,
    pub invert_scroll_direction: Option<InvertScrollDirection>,
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum InvertScrollDirection {
    #[default]
    All,
    Mouse,
    Trackpad,
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum WindowTitleMode {
    #[default]
    Title,
    Class,
    InitialTitle,
    InitialClass,
}

#[derive(Deserialize, Copy, Clone, Debug)]
#[serde(default)]
pub struct WindowTitleConfig {
    pub mode: WindowTitleMode,
    pub truncate_title_after_length: u32,
}

impl Default for WindowTitleConfig {
    fn default() -> Self {
        Self {
            mode: Default::default(),
            truncate_title_after_length: 150,
        }
    }
}

#[derive(Deserialize, Clone, Default, Debug)]
#[serde(default)]
pub struct KeyboardLayoutModuleConfig {
    pub labels: HashMap<String, String>,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct SystemInfoCpu {
    pub warn_threshold: u32,
    pub alert_threshold: u32,

    pub format: CpuFormat,
}

fn validate_thresholds<T: PartialOrd + Copy + std::fmt::Display>(
    warn: &mut T,
    alert: &mut T,
    name: &str,
) {
    if *warn >= *alert {
        warn!(
            "{name} warn_threshold ({warn}) >= alert_threshold ({alert}), setting both to {alert}"
        );
        *warn = *alert;
    }
}

impl SystemInfoCpu {
    fn validate(&mut self) {
        validate_thresholds(&mut self.warn_threshold, &mut self.alert_threshold, "CPU");
    }
}

impl Default for SystemInfoCpu {
    fn default() -> Self {
        Self {
            warn_threshold: 60,
            alert_threshold: 80,
            format: CpuFormat::Percentage,
        }
    }
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct SystemInfoMemory {
    pub warn_threshold: u32,
    pub alert_threshold: u32,
    pub format: MemoryFormat,
}

impl SystemInfoMemory {
    fn validate(&mut self) {
        validate_thresholds(
            &mut self.warn_threshold,
            &mut self.alert_threshold,
            "Memory",
        );
    }
}

impl Default for SystemInfoMemory {
    fn default() -> Self {
        Self {
            warn_threshold: 70,
            alert_threshold: 85,
            format: MemoryFormat::Percentage,
        }
    }
}

const DEFAULT_TEMP_WARN_CELSIUS: i32 = 60;
const DEFAULT_TEMP_ALERT_CELSIUS: i32 = 80;

#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq)]
pub enum TemperatureSensorType {
    #[default]
    Cpu,
    Gpu,
    Acpi,
    Nvme,
}

/// A type keyword (auto-detected) or an exact sensor label.
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum TemperatureSensor {
    Type(TemperatureSensorType),
    Label(String),
}

impl Default for TemperatureSensor {
    fn default() -> Self {
        Self::Type(TemperatureSensorType::default())
    }
}

#[derive(Deserialize, Clone, Debug, Default)]
#[serde(default)]
pub struct SystemInfoTemperature {
    warn_threshold: Option<i32>,
    alert_threshold: Option<i32>,
    pub sensor: TemperatureSensor,
}

impl SystemInfoTemperature {
    pub fn warn_threshold(&self) -> i32 {
        self.warn_threshold
            .unwrap_or_else(|| match crate::i18n::unit_system() {
                UnitSystem::Metric => DEFAULT_TEMP_WARN_CELSIUS,
                UnitSystem::Imperial => celsius_to_fahrenheit(DEFAULT_TEMP_WARN_CELSIUS),
            })
    }

    pub fn alert_threshold(&self) -> i32 {
        self.alert_threshold
            .unwrap_or_else(|| match crate::i18n::unit_system() {
                UnitSystem::Metric => DEFAULT_TEMP_ALERT_CELSIUS,
                UnitSystem::Imperial => celsius_to_fahrenheit(DEFAULT_TEMP_ALERT_CELSIUS),
            })
    }

    fn validate(&mut self) {
        if let (Some(warn), Some(alert)) = (&mut self.warn_threshold, &mut self.alert_threshold) {
            validate_thresholds(warn, alert, "Temperature");
        }
    }
}

#[derive(Clone, Debug, Deserialize, Default)]
pub enum DiskFormat {
    #[default]
    Percentage,
    Fraction,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub enum MemoryFormat {
    #[default]
    Percentage,
    Fraction,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub enum CpuFormat {
    #[default]
    Percentage,
    Frequency,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct SystemInfoDisk {
    pub warn_threshold: u32,
    pub alert_threshold: u32,
    pub format: DiskFormat,
    pub mounts: Option<Vec<String>>,
}

impl SystemInfoDisk {
    fn validate(&mut self) {
        validate_thresholds(&mut self.warn_threshold, &mut self.alert_threshold, "Disk");
    }
}

impl Default for SystemInfoDisk {
    fn default() -> Self {
        Self {
            warn_threshold: 80,
            alert_threshold: 90,
            format: DiskFormat::Percentage,
            mounts: None,
        }
    }
}

#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SystemInfoDiskIndicatorConfig {
    #[serde(rename = "Disk")]
    pub path: String,
    #[serde(rename = "Name")]
    pub name: Option<String>,
}

#[derive(Clone, Debug, Deserialize)]
pub enum SystemInfoIndicator {
    Cpu,
    Memory,
    MemorySwap,
    Temperature,
    IpAddress,
    DownloadSpeed,
    UploadSpeed,
    #[serde(untagged)]
    Disk(SystemInfoDiskIndicatorConfig),
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct SystemInfoModuleConfig {
    pub indicators: Vec<SystemInfoIndicator>,
    #[serde(default = "SystemInfoModuleConfig::default_interval")]
    pub interval: u64,
    pub cpu: SystemInfoCpu,
    pub memory: SystemInfoMemory,
    pub temperature: SystemInfoTemperature,
    pub disk: SystemInfoDisk,
}

impl SystemInfoModuleConfig {
    const fn default_interval() -> u64 {
        5
    }

    fn validate(&mut self) {
        if self.interval == 0 {
            warn!("SystemInfoModuleConfig.interval is 0, setting to 1");
            self.interval = 1;
        }
        self.cpu.validate();
        self.memory.validate();
        self.temperature.validate();
        self.disk.validate();
    }
}

impl Default for SystemInfoModuleConfig {
    fn default() -> Self {
        Self {
            indicators: vec![
                SystemInfoIndicator::Cpu,
                SystemInfoIndicator::Memory,
                SystemInfoIndicator::Temperature,
            ],
            interval: Self::default_interval(),
            cpu: SystemInfoCpu::default(),
            memory: SystemInfoMemory::default(),
            temperature: SystemInfoTemperature::default(),
            disk: SystemInfoDisk::default(),
        }
    }
}

#[derive(Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ToastPosition {
    TopLeft,
    #[default]
    TopRight,
    BottomLeft,
    BottomRight,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct NotificationsModuleConfig {
    pub format: String,
    pub show_timestamps: bool,
    pub show_bodies: bool,
    pub grouped: bool,
    pub toast: bool,
    pub toast_position: ToastPosition,
    pub toast_timeout: u64,
    pub toast_limit: usize,
    pub toast_max_height: u32,
    pub blocklist: Vec<RegexCfg>,
}
impl Default for NotificationsModuleConfig {
    fn default() -> Self {
        Self {
            format: "%H:%M".to_string(),
            show_timestamps: true,
            show_bodies: true,
            grouped: false,
            toast: true,
            toast_position: ToastPosition::default(),
            toast_timeout: 5000,
            toast_limit: 5,
            toast_max_height: 150,
            blocklist: vec![],
        }
    }
}

#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TrayClickAction {
    Open,
    Menu,
}

#[derive(Deserialize, Clone, Debug, Default)]
#[serde(default)]
pub struct TrayModuleConfig {
    pub blocklist: Vec<RegexCfg>,
    pub right_click: Option<TrayClickAction>,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct TempoModuleConfig {
    pub clock_format: String,
    #[serde(default)]
    pub formats: Vec<String>,
    #[serde(default)]
    pub timezones: Vec<String>,
    #[serde(default)]
    pub weather_location: Option<WeatherLocation>,
    pub weather_indicator: WeatherIndicator,
    #[serde(default)]
    pub wind_speed_unit: Option<WindSpeedUnit>,
}

impl TempoModuleConfig {
    pub fn resolved_wind_speed_unit(&self) -> WindSpeedUnit {
        match self.wind_speed_unit {
            Some(unit) => unit,
            None => match unit_system() {
                UnitSystem::Imperial => WindSpeedUnit::Mph,
                UnitSystem::Metric => WindSpeedUnit::Kmh,
            },
        }
    }

    fn validate(&mut self) {
        if let Some(WeatherLocation::Coordinates(lat, lon)) = &mut self.weather_location {
            let clamped_lat = lat.clamp(-90.0, 90.0);
            let clamped_lon = lon.clamp(-180.0, 180.0);
            if *lat != clamped_lat || *lon != clamped_lon {
                warn!(
                    "tempo.weather_location latitude ({lat}) must be in -90..=90 and longitude ({lon}) in -180..=180, setting to ({clamped_lat}, {clamped_lon})"
                );
                *lat = clamped_lat;
                *lon = clamped_lon;
            }
        }
    }
}

#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum WindSpeedUnit {
    #[default]
    Kmh,
    Mph,
    Ms,
}

impl WindSpeedUnit {
    pub fn symbol(self) -> &'static str {
        match self {
            Self::Kmh => "km/h",
            Self::Mph => "mph",
            Self::Ms => "m/s",
        }
    }

    pub fn api_param(self) -> &'static str {
        match self {
            Self::Kmh => "kmh",
            Self::Mph => "mph",
            Self::Ms => "ms",
        }
    }
}

#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
pub enum WeatherIndicator {
    #[default]
    IconAndTemperature,
    Icon,
    None,
}

#[derive(Deserialize, Default, Clone, Debug, PartialEq)]
pub enum WeatherLocation {
    #[default]
    Current,
    City(String),
    Coordinates(f32, f32),
}

impl std::hash::Hash for WeatherLocation {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        std::mem::discriminant(self).hash(state);
        match self {
            WeatherLocation::Current => {}
            WeatherLocation::City(city) => city.hash(state),
            WeatherLocation::Coordinates(lat, lon) => {
                lat.to_bits().hash(state);
                lon.to_bits().hash(state);
            }
        }
    }
}

impl Default for TempoModuleConfig {
    fn default() -> Self {
        Self {
            clock_format: "%a %d %b %R".to_string(),
            formats: vec![],
            timezones: vec![],
            weather_location: None,
            weather_indicator: WeatherIndicator::IconAndTemperature,
            wind_speed_unit: None,
        }
    }
}

#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum SettingsIndicator {
    IdleInhibitor,
    PowerProfile,
    Audio,
    Microphone,
    Network,
    Vpn,
    Bluetooth,
    Battery,
    PeripheralBattery,
    Brightness,
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum SettingsFormat {
    Icon,
    #[serde(alias = "Value")]
    Percentage,
    #[default]
    #[serde(alias = "IconAndValue")]
    IconAndPercentage,
    Time,
    IconAndTime,
    Name,
    IconAndName,
}

#[derive(Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub enum PeripheralIndicators {
    #[default]
    All,
    Specific(Vec<PeripheralDeviceKind>),
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct SettingsModuleConfig {
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub lock_cmd: Option<String>,
    pub shutdown_cmd: String,
    pub suspend_cmd: String,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub hibernate_cmd: Option<String>,
    pub reboot_cmd: String,
    pub logout_cmd: String,
    pub battery_format: SettingsFormat,
    pub battery_hide_when_full: bool,
    pub peripheral_indicators: PeripheralIndicators,
    pub peripheral_battery_format: SettingsFormat,
    pub peripheral_expanded_by_default: bool,
    pub volume_step: u8,
    pub max_volume: u8,
    pub audio_indicator_format: SettingsFormat,
    pub microphone_indicator_format: SettingsFormat,
    pub network_indicator_format: SettingsFormat,
    pub bluetooth_indicator_format: SettingsFormat,
    pub brightness_indicator_format: SettingsFormat,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub audio_sinks_more_cmd: Option<String>,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub audio_sources_more_cmd: Option<String>,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub wifi_more_cmd: Option<String>,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub vpn_more_cmd: Option<String>,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub bluetooth_more_cmd: Option<String>,
    pub remove_airplane_btn: bool,
    pub remove_idle_btn: bool,
    pub enable_tooltips: bool,
    pub indicators: Vec<SettingsIndicator>,
    #[serde(rename = "CustomButton")]
    pub custom_buttons: Vec<SettingsCustomButton>,
}

impl Default for SettingsModuleConfig {
    fn default() -> Self {
        Self {
            lock_cmd: Default::default(),
            shutdown_cmd: "shutdown now".to_string(),
            suspend_cmd: "systemctl suspend".to_string(),
            hibernate_cmd: Default::default(),
            reboot_cmd: "systemctl reboot".to_string(),
            logout_cmd: "loginctl kill-user $(whoami)".to_string(),
            battery_format: SettingsFormat::IconAndPercentage,
            battery_hide_when_full: false,
            peripheral_indicators: Default::default(),
            peripheral_battery_format: SettingsFormat::Icon,
            peripheral_expanded_by_default: false,
            volume_step: 5,
            max_volume: 100,
            audio_indicator_format: SettingsFormat::Icon,
            microphone_indicator_format: SettingsFormat::Icon,
            network_indicator_format: SettingsFormat::Icon,
            bluetooth_indicator_format: SettingsFormat::Icon,
            brightness_indicator_format: SettingsFormat::Icon,
            audio_sinks_more_cmd: Default::default(),
            audio_sources_more_cmd: Default::default(),
            wifi_more_cmd: Default::default(),
            vpn_more_cmd: Default::default(),
            bluetooth_more_cmd: Default::default(),
            remove_airplane_btn: Default::default(),
            remove_idle_btn: Default::default(),
            enable_tooltips: true,
            indicators: vec![
                SettingsIndicator::IdleInhibitor,
                SettingsIndicator::PowerProfile,
                SettingsIndicator::Audio,
                SettingsIndicator::Microphone,
                SettingsIndicator::Bluetooth,
                SettingsIndicator::Network,
                SettingsIndicator::Vpn,
                SettingsIndicator::Battery,
            ],
            custom_buttons: Default::default(),
        }
    }
}

impl SettingsModuleConfig {
    fn validate(&mut self) {
        if self.max_volume == 0 || self.max_volume > 200 {
            warn!(
                "settings.max_volume ({}) out of range 1..=200, clamping to 100",
                self.max_volume
            );
            self.max_volume = 100;
        }
        if self.volume_step == 0 || self.volume_step > 50 {
            warn!(
                "settings.volume_step ({}) out of range 1..=50, clamping to 5",
                self.volume_step
            );
            self.volume_step = 5;
        }
    }
}

#[derive(Deserialize, Clone, Debug)]
pub struct SettingsCustomButton {
    pub name: String,
    pub icon: String,
    pub command: String,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub status_command: Option<String>,
    pub tooltip: Option<String>,
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum MediaPlayerFormat {
    Icon,
    #[serde(alias = "Title")]
    Text,
    #[serde(alias = "IconAndTitle")]
    #[default]
    IconAndText,
}

#[derive(Deserialize, Copy, Clone, PartialEq, Eq, Debug)]
pub enum MediaPlayerTextField {
    Artist,
    Title,
    Album,
}

#[derive(Deserialize, Copy, Clone, PartialEq, Eq, Debug)]
pub enum MediaPlayerVisualizer {
    Background,
    Before,
    After,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct MediaPlayerModuleConfig {
    pub indicator_format: MediaPlayerFormat,
    pub indicator_fields: Vec<MediaPlayerTextField>,
    pub max_text_length: u32,
    pub indicator_visualizer: Option<MediaPlayerVisualizer>,
    pub menu_visualizer: bool,
    pub visualizer_framerate: u32,
}

impl Default for MediaPlayerModuleConfig {
    fn default() -> Self {
        MediaPlayerModuleConfig {
            indicator_format: MediaPlayerFormat::default(),
            indicator_fields: vec![MediaPlayerTextField::Artist, MediaPlayerTextField::Title],
            max_text_length: 100,
            indicator_visualizer: None,
            menu_visualizer: false,
            visualizer_framerate: Self::DEFAULT_VISUALIZER_FRAMERATE,
        }
    }
}

impl MediaPlayerModuleConfig {
    const DEFAULT_VISUALIZER_FRAMERATE: u32 = 30;
    const MAX_VISUALIZER_FRAMERATE: u32 = 144;

    fn validate(&mut self) {
        let clamped = self
            .visualizer_framerate
            .clamp(1, Self::MAX_VISUALIZER_FRAMERATE);
        if clamped != self.visualizer_framerate {
            warn!(
                "MediaPlayerModuleConfig.visualizer_framerate is {}, setting to {clamped}",
                self.visualizer_framerate
            );
            self.visualizer_framerate = clamped;
        }
    }
}

fn hex_to_color(hex: HexColor) -> Color {
    Color::from_rgb8(hex.r, hex.g, hex.b)
}

fn hex_to_pair(hex: HexColor, text: Option<HexColor>, text_fallback: Color) -> palette::Pair {
    palette::Pair::new(
        hex_to_color(hex),
        text.map(hex_to_color).unwrap_or(text_fallback),
    )
}

#[derive(Deserialize, Clone, Copy, Debug)]
#[serde(untagged)]
pub enum AppearanceColor {
    Simple(HexColor),
    Complete {
        base: HexColor,
        strong: Option<HexColor>,
        weak: Option<HexColor>,
        text: Option<HexColor>,
    },
}

impl AppearanceColor {
    pub fn get_base(&self) -> Color {
        match self {
            AppearanceColor::Simple(color) => hex_to_color(*color),
            AppearanceColor::Complete { base, .. } => hex_to_color(*base),
        }
    }

    pub fn get_text(&self) -> Option<Color> {
        match self {
            AppearanceColor::Simple(_) => None,
            AppearanceColor::Complete { text, .. } => text.map(hex_to_color),
        }
    }

    pub fn get_weak_pair(&self, text_fallback: Color) -> Option<palette::Pair> {
        match self {
            AppearanceColor::Simple(_) => None,
            AppearanceColor::Complete { weak, text, .. } => {
                weak.map(|color| hex_to_pair(color, *text, text_fallback))
            }
        }
    }

    pub fn get_strong_pair(&self, text_fallback: Color) -> Option<palette::Pair> {
        match self {
            AppearanceColor::Simple(_) => None,
            AppearanceColor::Complete { strong, text, .. } => {
                strong.map(|color| hex_to_pair(color, *text, text_fallback))
            }
        }
    }
}

#[derive(Deserialize, Clone, Copy, Debug)]
#[serde(untagged)]
pub enum BackgroundAppearanceColor {
    Simple(HexColor),
    Complete {
        base: HexColor,
        weakest: Option<HexColor>,
        weaker: Option<HexColor>,
        weak: Option<HexColor>,
        neutral: Option<HexColor>,
        strong: Option<HexColor>,
        stronger: Option<HexColor>,
        strongest: Option<HexColor>,
        text: Option<HexColor>,
    },
}

impl BackgroundAppearanceColor {
    pub fn get_base(&self) -> Color {
        match self {
            BackgroundAppearanceColor::Simple(color) => hex_to_color(*color),
            BackgroundAppearanceColor::Complete { base, .. } => hex_to_color(*base),
        }
    }

    pub fn get_text(&self) -> Option<Color> {
        match self {
            BackgroundAppearanceColor::Simple(_) => None,
            BackgroundAppearanceColor::Complete { text, .. } => text.map(hex_to_color),
        }
    }

    pub fn get_pair(&self, level: BackgroundLevel, text_fallback: Color) -> Option<palette::Pair> {
        match self {
            BackgroundAppearanceColor::Simple(_) => None,
            BackgroundAppearanceColor::Complete {
                weakest,
                weaker,
                weak,
                neutral,
                strong,
                stronger,
                strongest,
                text,
                ..
            } => {
                let hex = match level {
                    BackgroundLevel::Weakest => *weakest,
                    BackgroundLevel::Weaker => *weaker,
                    BackgroundLevel::Weak => *weak,
                    BackgroundLevel::Neutral => *neutral,
                    BackgroundLevel::Strong => *strong,
                    BackgroundLevel::Stronger => *stronger,
                    BackgroundLevel::Strongest => *strongest,
                };
                hex.map(|h| hex_to_pair(h, *text, text_fallback))
            }
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub enum BackgroundLevel {
    Weakest,
    Weaker,
    Weak,
    Neutral,
    Strong,
    Stronger,
    Strongest,
}

#[derive(Deserialize, Default, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum BarSurface {
    #[default]
    Transparent,
    Solid,
}

#[derive(Deserialize, Default, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum RadiusSize {
    #[default]
    None,
    Sm,
    Md,
    Lg,
    Xl,
}

#[derive(Deserialize, Default, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum SpaceSize {
    #[default]
    None,
    Xxs,
    Xs,
    Sm,
    Md,
    Lg,
    Xl,
    Xxl,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum CssShorthand<T> {
    One(T),
    Many(Vec<T>),
}

impl<T: Copy> CssShorthand<T> {
    fn expand<E: serde::de::Error>(self) -> Result<[T; 4], E> {
        let values = match self {
            CssShorthand::One(v) => return Ok([v, v, v, v]),
            CssShorthand::Many(v) => v,
        };
        match values.as_slice() {
            [a] => Ok([*a, *a, *a, *a]),
            [a, b] => Ok([*a, *b, *a, *b]),
            [a, b, c, d] => Ok([*a, *b, *c, *d]),
            _ => Err(E::custom("expected 1, 2 or 4 values (CSS shorthand)")),
        }
    }
}

/// Per-corner radius selection, deserialized with CSS `border-radius` shorthand:
/// 1 value = all corners, 2 = `[top-left+bottom-right, top-right+bottom-left]`,
/// 4 = `[top-left, top-right, bottom-right, bottom-left]`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct BarRadius {
    pub top_left: RadiusSize,
    pub top_right: RadiusSize,
    pub bottom_right: RadiusSize,
    pub bottom_left: RadiusSize,
}

impl<'de> Deserialize<'de> for BarRadius {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let [top_left, top_right, bottom_right, bottom_left] =
            CssShorthand::<RadiusSize>::deserialize(deserializer)?.expand()?;
        Ok(Self {
            top_left,
            top_right,
            bottom_right,
            bottom_left,
        })
    }
}

/// Per-edge margin selection, deserialized with CSS `margin` shorthand:
/// 1 value = all edges, 2 = `[vertical, horizontal]`, 4 = `[top, right, bottom, left]`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct BarMargin {
    pub top: SpaceSize,
    pub right: SpaceSize,
    pub bottom: SpaceSize,
    pub left: SpaceSize,
}

impl<'de> Deserialize<'de> for BarMargin {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let [top, right, bottom, left] =
            CssShorthand::<SpaceSize>::deserialize(deserializer)?.expand()?;
        Ok(Self {
            top,
            right,
            bottom,
            left,
        })
    }
}

#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq)]
#[serde(default)]
pub struct BarAppearance {
    pub surface: BarSurface,
    pub radius: BarRadius,
    pub margin: BarMargin,
}

#[derive(Deserialize, Default, Clone, Copy, Debug)]
#[serde(default)]
pub struct MenuAppearance {
    pub backdrop: f32,
}

/// A layer-shell surface. Each one is drawn with its own theme.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Surface {
    Bar,
    Menu,
    Osd,
    Notifications,
}

impl Surface {
    pub const ALL: [Surface; 4] = [
        Surface::Bar,
        Surface::Menu,
        Surface::Osd,
        Surface::Notifications,
    ];
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Opacity {
    default: f32,
    bar: Option<f32>,
    menu: Option<f32>,
    osd: Option<f32>,
    notifications: Option<f32>,
}

impl Opacity {
    pub fn get(&self, surface: Surface) -> f32 {
        match surface {
            Surface::Bar => self.bar,
            Surface::Menu => self.menu,
            Surface::Osd => self.osd,
            Surface::Notifications => self.notifications,
        }
        .unwrap_or(self.default)
    }
}

impl Default for Opacity {
    fn default() -> Self {
        Self {
            default: 1.0,
            bar: None,
            menu: None,
            osd: None,
            notifications: None,
        }
    }
}

impl<'de> Deserialize<'de> for Opacity {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct OpacityVisitor;

        impl<'de> Visitor<'de> for OpacityVisitor {
            type Value = Opacity;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str(
                    "a number between 0.0 and 1.0, or a table with `default` and per-surface overrides",
                )
            }

            fn visit_f64<E: de::Error>(self, v: f64) -> Result<Opacity, E> {
                Ok(Opacity {
                    default: check_opacity(v).map_err(E::custom)?,
                    ..Opacity::default()
                })
            }

            fn visit_i64<E: de::Error>(self, v: i64) -> Result<Opacity, E> {
                self.visit_f64(v as f64)
            }

            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Opacity, E> {
                self.visit_f64(v as f64)
            }

            fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Opacity, M::Error> {
                #[derive(Deserialize)]
                struct Table {
                    default: Option<f64>,
                    bar: Option<f64>,
                    menu: Option<f64>,
                    osd: Option<f64>,
                    notifications: Option<f64>,
                }

                let table = Table::deserialize(de::value::MapAccessDeserializer::new(map))?;
                let check =
                    |v: Option<f64>| v.map(check_opacity).transpose().map_err(de::Error::custom);

                Ok(Opacity {
                    default: check(table.default)?.unwrap_or(1.0),
                    bar: check(table.bar)?,
                    menu: check(table.menu)?,
                    osd: check(table.osd)?,
                    notifications: check(table.notifications)?,
                })
            }
        }

        deserializer.deserialize_any(OpacityVisitor)
    }
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct Appearance {
    pub font_name: Option<String>,
    #[serde(deserialize_with = "scale_factor_deserializer")]
    pub scale_factor: f64,
    pub opacity: Opacity,
    pub bar: BarAppearance,
    pub menu: MenuAppearance,
    pub background_color: BackgroundAppearanceColor,
    pub primary_color: AppearanceColor,
    pub success_color: AppearanceColor,
    pub warning_color: AppearanceColor,
    pub danger_color: AppearanceColor,
    pub text_color: AppearanceColor,
    pub workspace_colors: Vec<AppearanceColor>,
    pub special_workspace_colors: Option<Vec<AppearanceColor>>,
    /// Blur the wallpaper behind ashell's translucent surfaces via
    /// `ext-background-effect-v1`. No-op where the protocol is unsupported.
    pub blur: BlurMode,
}

/// When to ask the compositor for background blur.
#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum BlurMode {
    /// Blur when `opacity < 1`; blurring an opaque surface would not show.
    #[default]
    Auto,
    /// Always ask, even at full opacity.
    Always,
    /// Never ask.
    Never,
}

impl BlurMode {
    pub fn enabled(self, opacity: f32) -> bool {
        match self {
            Self::Auto => opacity < 1.0,
            Self::Always => true,
            Self::Never => false,
        }
    }
}

static PRIMARY: HexColor = HexColor::rgb(122, 162, 247);

fn scale_factor_deserializer<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let v = f64::deserialize(deserializer)?;

    if v <= 0.0 {
        return Err(serde::de::Error::custom(
            "Scale factor must be greater than 0.0",
        ));
    }

    if v > 2.0 {
        return Err(serde::de::Error::custom(
            "Scale factor cannot be greater than 2.0",
        ));
    }

    Ok(v)
}

fn check_opacity(v: f64) -> Result<f32, &'static str> {
    if v < 0.0 {
        return Err("Opacity cannot be negative");
    }

    if v > 1.0 {
        return Err("Opacity cannot be greater than 1.0");
    }

    Ok(v as f32)
}

impl Default for Appearance {
    fn default() -> Self {
        Self {
            font_name: None,
            scale_factor: 1.0,
            opacity: Opacity::default(),
            bar: BarAppearance::default(),
            menu: MenuAppearance::default(),
            background_color: BackgroundAppearanceColor::Complete {
                base: HexColor::rgb(26, 27, 38),
                weakest: None,
                weaker: None,
                weak: Some(HexColor::rgb(36, 39, 58)),
                neutral: None,
                strong: Some(HexColor::rgb(65, 72, 104)),
                stronger: None,
                strongest: None,
                text: None,
            },
            primary_color: AppearanceColor::Simple(PRIMARY),
            success_color: AppearanceColor::Simple(HexColor::rgb(158, 206, 106)),
            warning_color: AppearanceColor::Simple(HexColor::rgb(224, 175, 104)),
            danger_color: AppearanceColor::Simple(HexColor::rgb(247, 118, 142)),
            text_color: AppearanceColor::Simple(HexColor::rgb(169, 177, 214)),
            workspace_colors: vec![
                AppearanceColor::Simple(PRIMARY),
                AppearanceColor::Simple(HexColor::rgb(158, 206, 106)),
            ],
            special_workspace_colors: None,
            blur: BlurMode::default(),
        }
    }
}

#[derive(Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Position {
    #[default]
    Top,
    Bottom,
}

#[derive(Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Layer {
    #[default]
    Bottom,
    Top,
    Overlay,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModuleName {
    Updates,
    Workspaces,
    WindowTitle,
    SystemInfo,
    KeyboardLayout,
    KeyboardSubmap,
    Tray,
    Tempo,
    Privacy,
    Settings,
    MediaPlayer,
    Custom(String),
    Notifications,
}

impl<'de> Deserialize<'de> for ModuleName {
    fn deserialize<D>(deserializer: D) -> Result<ModuleName, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct ModuleNameVisitor;
        impl Visitor<'_> for ModuleNameVisitor {
            type Value = ModuleName;
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a string representing a ModuleName")
            }
            fn visit_str<E>(self, value: &str) -> Result<ModuleName, E>
            where
                E: serde::de::Error,
            {
                Ok(match value {
                    "Updates" => ModuleName::Updates,
                    "Workspaces" => ModuleName::Workspaces,
                    "WindowTitle" => ModuleName::WindowTitle,
                    "SystemInfo" => ModuleName::SystemInfo,
                    "KeyboardLayout" => ModuleName::KeyboardLayout,
                    "KeyboardSubmap" => ModuleName::KeyboardSubmap,
                    "Tray" => ModuleName::Tray,
                    "Notifications" => ModuleName::Notifications,
                    "Tempo" => ModuleName::Tempo,
                    "Privacy" => ModuleName::Privacy,
                    "Settings" => ModuleName::Settings,
                    "MediaPlayer" => ModuleName::MediaPlayer,
                    other => ModuleName::Custom(other.to_string()),
                })
            }
        }
        deserializer.deserialize_str(ModuleNameVisitor)
    }
}

#[derive(Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum ModuleDef {
    Single(ModuleName),
    Group(Vec<ModuleName>),
}

#[derive(Deserialize, Clone, Debug)]
pub struct Modules {
    #[serde(default)]
    pub left: Vec<ModuleDef>,
    #[serde(default)]
    pub center: Vec<ModuleDef>,
    #[serde(default)]
    pub right: Vec<ModuleDef>,
}

impl Modules {
    pub fn contains(&self, target: &ModuleName) -> bool {
        self.left
            .iter()
            .chain(&self.center)
            .chain(&self.right)
            .any(|def| match def {
                ModuleDef::Single(m) => m == target,
                ModuleDef::Group(ms) => ms.contains(target),
            })
    }
}

impl Default for Modules {
    fn default() -> Self {
        Self {
            left: vec![ModuleDef::Single(ModuleName::Workspaces)],
            center: vec![ModuleDef::Single(ModuleName::WindowTitle)],
            right: vec![ModuleDef::Group(vec![
                ModuleName::Tempo,
                ModuleName::Privacy,
                ModuleName::Settings,
            ])],
        }
    }
}

#[derive(Deserialize, Clone, Default, Debug, PartialEq, Eq)]
pub enum Outputs {
    #[default]
    All,
    Active,
    #[serde(deserialize_with = "non_empty")]
    Targets(Vec<String>),
}

fn non_empty<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    let vec = <Vec<T>>::deserialize(d)?;
    if vec.is_empty() {
        use serde::de::Error;

        Err(D::Error::custom("need non-empty"))
    } else {
        Ok(vec)
    }
}

fn empty_string_as_none<'de, D>(d: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    Ok(Option::<String>::deserialize(d)?
        .and_then(|value| (!value.trim().is_empty()).then_some(value)))
}

/// Newtype wrapper around `Regex`to be deserializable and usable as a hashmap key
#[serde_as]
#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
pub struct RegexCfg(#[serde_as(as = "DisplayFromStr")] pub Regex);

impl PartialEq for RegexCfg {
    fn eq(&self, other: &Self) -> bool {
        self.0.as_str() == other.0.as_str()
    }
}
impl Eq for RegexCfg {}

impl std::hash::Hash for RegexCfg {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        // hash the raw pattern string
        self.0.as_str().hash(state);
    }
}

impl Deref for RegexCfg {
    type Target = Regex;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Deserialize, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum CustomModuleType {
    #[default]
    Button,
    Text,
}

#[serde_as]
#[derive(Deserialize, Clone, Debug)]
pub struct CustomModuleDef {
    pub name: String,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub command: Option<String>,
    #[serde(default)]
    pub icon: Option<String>,

    /// yields json lines containing text, alt, (pot tooltip)
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub listen_cmd: Option<String>,
    /// map of regex -> icon
    pub icons: Option<HashMap<RegexCfg, String>>,
    /// regex to show alert
    pub alert: Option<RegexCfg>,
    /// Display type: Button (clickable) or Text (display only)
    #[serde(default)]
    pub r#type: CustomModuleType,
    /// command to run on right-click
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub on_right_click: Option<String>,
    /// command to run on middle-click
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub on_middle_click: Option<String>,
    /// command to run on scroll up
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub on_scroll_up: Option<String>,
    /// command to run on scroll down
    #[serde(default, deserialize_with = "empty_string_as_none")]
    pub on_scroll_down: Option<String>,
    // .. appearance etc
}

#[derive(Deserialize, Clone, Debug)]
#[serde(default)]
pub struct OsdConfig {
    pub enabled: bool,
    pub timeout: u64,
    pub show_volume_percentage: bool,
    pub show_brightness_percentage: bool,
}

impl Default for OsdConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            timeout: 1500,
            show_volume_percentage: false,
            show_brightness_percentage: false,
        }
    }
}

/// Parses just the `[logging]` table, before the logger exists: no `log::*`
/// here, problems go to `stderr`.
pub fn read_logging_config(path: Option<&PathBuf>) -> LoggingConfig {
    #[derive(Deserialize)]
    struct LoggingConfigWrapper {
        #[serde(default)]
        logging: LoggingConfig,
    }

    let config_path = match resolve_config_path(path.map(PathBuf::as_path)) {
        Ok(p) => p,
        Err(e) => {
            eprintln!(
                "ashell: warning: cannot expand the config path: {e}, using the default logging setup"
            );
            return LoggingConfig::default();
        }
    };

    let content = match std::fs::read_to_string(&config_path) {
        Ok(c) => c,
        Err(e) => {
            if e.kind() != std::io::ErrorKind::NotFound {
                eprintln!(
                    "ashell: warning: cannot read {}: {e}, using the default logging setup",
                    config_path.display()
                );
            }
            return LoggingConfig::default();
        }
    };

    match toml::from_str::<LoggingConfigWrapper>(&content) {
        Ok(wrapper) => wrapper.logging,
        Err(e) => {
            eprintln!(
                "ashell: warning: cannot read the [logging] section of {}, using the default logging setup:\n{e}",
                config_path.display()
            );
            LoggingConfig::default()
        }
    }
}

fn resolve_config_path(path: Option<&Path>) -> Result<PathBuf, Box<dyn Error + Send>> {
    expand_path(match path {
        Some(p) => p.to_path_buf(),
        None => PathBuf::from(DEFAULT_CONFIG_FILE_PATH),
    })
}

pub fn get_config(path: Option<PathBuf>) -> Result<(Config, PathBuf), Box<dyn Error + Send>> {
    let explicit = path.is_some();
    let expanded = resolve_config_path(path.as_deref())?;

    if explicit {
        info!("Config path provided {expanded:?}");

        if !expanded.exists() {
            return Err(Box::new(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("Config file does not exist: {}", expanded.display()),
            )));
        }
    } else {
        // DEFAULT_CONFIG_FILE_PATH has a parent and shellexpand never strips components.
        let parent = expanded
            .parent()
            .expect("Failed to get default config parent directory");

        if !parent.exists() {
            std::fs::create_dir_all(parent)
                .expect("Failed to create default config parent directory");
        }
    }

    Ok((read_config(&expanded).unwrap_or_default(), expanded))
}

fn expand_path(path: PathBuf) -> Result<PathBuf, Box<dyn Error + Send>> {
    let str_path = path.to_string_lossy();
    let expanded =
        shellexpand::full(&str_path).map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;

    Ok(PathBuf::from(expanded.to_string()))
}

fn read_config(path: &Path) -> Result<Config, Box<dyn Error + Send>> {
    let content =
        std::fs::read_to_string(path).map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;

    info!("Decoding config file {path:?}");

    let de =
        toml::Deserializer::parse(&content).map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    let mut unknown_fields = Vec::new();
    let res = serde_ignored::deserialize(de, |path| {
        unknown_fields.push(path.to_string());
    });

    match res {
        Ok(config) => {
            for field in &unknown_fields {
                let msg = format!("Unknown configuration field ignored: {field}");
                warn!("{msg}");
                eprintln!("ashell: warning: {msg}");
            }
            info!("Config file loaded successfully");
            let mut config: Config = config;
            config.validate();
            Ok(config)
        }
        Err(e) => {
            warn!("Failed to parse config file: {e}");
            Err(Box::new(e))
        }
    }
}

enum Event {
    Changed,
    Removed,
}

pub fn subscription(path: &Path) -> Subscription<Message> {
    let path = path.to_path_buf();

    Subscription::run_with(path, |path| {
        let path = std::fs::canonicalize(path).unwrap_or_else(|_| path.clone());
        channel(100, async move |mut output| {
            match (path.parent(), path.file_name(), Inotify::init()) {
                (Some(folder), Some(file_name), Ok(inotify)) => {
                    debug!("Watching config file at {path:?}");

                    let res = inotify.watches().add(
                        folder,
                        WatchMask::CREATE | WatchMask::DELETE | WatchMask::MOVE | WatchMask::MODIFY,
                    );

                    if let Err(e) = res {
                        error!("Failed to add watch for {folder:?}: {e}");
                        return;
                    }

                    let buffer = [0; 1024];
                    let stream = inotify.into_event_stream(buffer);

                    if let Ok(stream) = stream {
                        let mut stream = stream.ready_chunks(10);

                        debug!("Starting config file watch loop");

                        loop {
                            let events = stream.next().await.unwrap_or(vec![]);

                            debug!("Received inotify events: {events:?}");

                            let mut file_event = None;

                            for event in events {
                                debug!("Event: {event:?}");
                                match event {
                                    Ok(inotify::Event {
                                        name: Some(name),
                                        mask: EventMask::DELETE | EventMask::MOVED_FROM,
                                        ..
                                    }) if file_name == name => {
                                        debug!("File deleted or moved");
                                        file_event = Some(Event::Removed);
                                    }
                                    Ok(inotify::Event {
                                        name: Some(name),
                                        mask:
                                            EventMask::CREATE | EventMask::MODIFY | EventMask::MOVED_TO,
                                        ..
                                    }) if file_name == name => {
                                        debug!("File created or moved");

                                        file_event = Some(Event::Changed);
                                    }
                                    _ => {
                                        debug!("Ignoring event");
                                    }
                                }
                            }

                            match file_event {
                                Some(Event::Changed) => {
                                    info!("Reload config file");

                                    let path_clone = path.clone();
                                    let new_config = tokio::task::spawn_blocking(move || {
                                        read_config(&path_clone).unwrap_or_default()
                                    })
                                    .await
                                    .unwrap_or_default();

                                    let _ = output
                                        .send(Message::ConfigChanged(Box::new(new_config)))
                                        .await;
                                }
                                Some(Event::Removed) => {
                                    // wait and double check if the file is really gone
                                    sleep(Duration::from_millis(500)).await;

                                    if !path.exists() {
                                        info!("Config file removed");
                                        let _ = output
                                            .send(Message::ConfigChanged(Box::default()))
                                            .await;
                                    }
                                }
                                None => {
                                    debug!("No relevant file event detected.");
                                }
                            }
                        }
                    } else {
                        error!("Failed to create inotify event stream");
                    }
                }
                (None, _, _) => {
                    error!(
                        "Config file path does not have a parent directory, cannot watch for changes"
                    );
                }
                (_, None, _) => {
                    error!("Config file path does not have a file name, cannot watch for changes");
                }
                (_, _, Err(e)) => {
                    error!("Failed to initialize inotify: {e}");
                }
            }
        })
    })
}
