use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;

use calloop::channel::{self, Sender};
use calloop::LoopHandle;
use tracing::{error, info};

use crate::forge::Forge;

/// Events sent from the D-Bus thread to the main event loop.
#[derive(Debug)]
pub enum DisplayConfigEvent {
    /// Set backlight brightness (0-100).
    SetBrightness(i32),
}

static SERIAL: AtomicU32 = AtomicU32::new(1);

/// Shared state readable from the D-Bus thread: brightness plus a
/// snapshot of the primary output (mode, scale, connector name) that the
/// compositor refreshes on init and on output resize, so GetCurrentState
/// can answer with real data instead of empty lists.
pub struct SharedDisplayState {
    pub brightness: AtomicU32,
    pub width: AtomicU32,
    pub height: AtomicU32,
    /// Refresh rate in millihertz.
    pub refresh_mhz: AtomicU32,
    /// Output scale × 1000 (matches the 1/120 wire quantisation).
    pub scale_milli: AtomicU32,
    pub connector: std::sync::Mutex<String>,
}

impl Default for SharedDisplayState {
    fn default() -> Self {
        Self {
            brightness: AtomicU32::new(100),
            width: AtomicU32::new(0),
            height: AtomicU32::new(0),
            refresh_mhz: AtomicU32::new(0),
            scale_milli: AtomicU32::new(1000),
            connector: std::sync::Mutex::new(String::new()),
        }
    }
}

impl SharedDisplayState {
    /// Refresh the output snapshot and bump the config serial.
    pub fn update(&self, connector: &str, w: u32, h: u32, refresh_mhz: u32, scale: f64) {
        self.width.store(w, Ordering::Relaxed);
        self.height.store(h, Ordering::Relaxed);
        self.refresh_mhz.store(refresh_mhz, Ordering::Relaxed);
        self.scale_milli.store((scale * 1000.0).round() as u32, Ordering::Relaxed);
        if let Ok(mut c) = self.connector.lock() {
            *c = connector.to_string();
        }
        SERIAL.fetch_add(1, Ordering::Relaxed);
    }
}

/// Start the org.gnome.Mutter.DisplayConfig D-Bus server. Returns the
/// shared snapshot so the compositor can refresh it when the output
/// changes.
pub fn start_server(event_loop: &LoopHandle<'static, Forge>) -> Arc<SharedDisplayState> {
    let (sender, channel) = channel::channel::<DisplayConfigEvent>();
    let shared = Arc::new(SharedDisplayState::default());
    let shared_clone = shared.clone();
    let shared_ret = shared.clone();

    event_loop
        .insert_source(channel, move |event, _, forge| {
            if let calloop::channel::Event::Msg(ev) = event {
                match ev {
                    DisplayConfigEvent::SetBrightness(level) => {
                        let clamped = level.clamp(0, 100);
                        forge.session.auto_brightness.set_brightness(clamped as u8);
                        forge.shell.osd.show(crate::osd::OsdKind::Brightness, clamped as u8, false);
                        forge.request_redraw();
                    }
                }
            }
        })
        .expect("display config channel");

    thread::Builder::new()
        .name("forge-display-config".into())
        .spawn(move || {
            if let Err(e) = run_display_config_dbus(sender, shared_clone) {
                error!("DisplayConfig D-Bus server failed: {e}");
            }
        })
        .expect("display config thread");

    shared_ret
}

fn run_display_config_dbus(
    sender: Sender<DisplayConfigEvent>,
    shared: Arc<SharedDisplayState>,
) -> Result<(), Box<dyn std::error::Error>> {
    info!("DisplayConfig D-Bus server starting");

    let display_config_iface = DisplayConfigIface {
        shared: shared.clone(),
    };
    let brightness_iface = BrightnessIface {
        sender: sender.clone(),
        shared,
    };

    let _conn = zbus::blocking::ConnectionBuilder::session()?
        .name("org.gnome.Mutter.DisplayConfig")?
        .serve_at("/org/gnome/Mutter/DisplayConfig", display_config_iface)?
        .serve_at("/org/gnome/Shell/Brightness", brightness_iface)?
        .build()?;

    loop {
        std::thread::sleep(std::time::Duration::from_secs(60));
    }
}

// --- org.gnome.Mutter.DisplayConfig interface ---

struct DisplayConfigIface {
    shared: Arc<SharedDisplayState>,
}

#[zbus::interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfigIface {
    /// Simplified monitor listing (not the full Mutter tuple shape):
    /// one monitor with its current mode and one logical monitor with
    /// the scale in the properties map. Empty until the compositor's
    /// first snapshot update lands.
    fn get_current_state(
        &self,
    ) -> (
        u32,                                           // serial
        Vec<(u32, i64, Vec<(String, u32, u32, f64)>)>, // monitors: (id, _, modes(name, w, h, refresh_hz))
        Vec<(u32, u32, u32, u32, Vec<u32>, HashMap<String, zbus::zvariant::OwnedValue>)>, // logical monitors
        HashMap<String, zbus::zvariant::OwnedValue>,   // properties
    )
    where
        HashMap<String, zbus::zvariant::OwnedValue>: Default,
    {
        let serial = SERIAL.load(Ordering::Relaxed);
        let w = self.shared.width.load(Ordering::Relaxed);
        let h = self.shared.height.load(Ordering::Relaxed);
        if w == 0 || h == 0 {
            return (serial, Vec::new(), Vec::new(), HashMap::new());
        }
        let refresh = self.shared.refresh_mhz.load(Ordering::Relaxed) as f64 / 1000.0;
        let scale = self.shared.scale_milli.load(Ordering::Relaxed) as f64 / 1000.0;
        let connector = self
            .shared
            .connector
            .lock()
            .map(|c| c.clone())
            .unwrap_or_default();

        let mode_name = format!("{w}x{h}@{refresh:.3}");
        let monitors = vec![(0u32, 0i64, vec![(mode_name, w, h, refresh)])];

        let lw = (w as f64 / scale).round() as u32;
        let lh = (h as f64 / scale).round() as u32;
        let mut props: HashMap<String, zbus::zvariant::OwnedValue> = HashMap::new();
        if let Ok(v) = zbus::zvariant::OwnedValue::try_from(zbus::zvariant::Value::from(scale)) {
            props.insert("scale".into(), v);
        }
        if let Ok(v) =
            zbus::zvariant::OwnedValue::try_from(zbus::zvariant::Value::from(connector))
        {
            props.insert("connector".into(), v);
        }
        let logical = vec![(0u32, 0u32, lw, lh, vec![0u32], props)];

        (serial, monitors, logical, HashMap::new())
    }

    #[zbus(property)]
    fn power_save_mode(&self) -> i32 {
        0 // On
    }

    #[zbus(property)]
    fn set_power_save_mode(&self, _mode: i32) {
        // Could map to DPMS control
    }

    #[zbus(signal)]
    async fn monitors_changed(ctxt: &zbus::SignalContext<'_>) -> zbus::Result<()>;
}

use std::collections::HashMap;

// --- org.gnome.Shell.Brightness interface ---

struct BrightnessIface {
    sender: Sender<DisplayConfigEvent>,
    shared: Arc<SharedDisplayState>,
}

#[zbus::interface(name = "org.gnome.Shell.Brightness")]
impl BrightnessIface {
    #[zbus(property)]
    fn brightness(&self) -> i32 {
        self.shared.brightness.load(Ordering::Relaxed) as i32
    }

    #[zbus(property)]
    fn set_brightness(&self, value: i32) {
        let clamped = value.clamp(0, 100);
        self.shared.brightness.store(clamped as u32, Ordering::Relaxed);
        let _ = self.sender.send(DisplayConfigEvent::SetBrightness(clamped));
    }
}
