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

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

use crate::forge::Forge;

#[derive(Debug)]
pub enum SessionEvent {
    EndSessionDialog { dialog_type: u32 },
    ClientRegistered { app_id: String },
    InhibitorAdded { reason: String },
}

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

pub fn start_server(event_loop: &LoopHandle<'static, Forge>) {
    let (sender, channel) = channel::channel::<SessionEvent>();

    event_loop
        .insert_source(channel, |event, _, forge| {
            if let calloop::channel::Event::Msg(ev) = event {
                match ev {
                    SessionEvent::EndSessionDialog { dialog_type } => match dialog_type {
                        0 => {
                            forge.power_menu.show_logout();
                            forge.request_redraw();
                        }
                        1 => {
                            forge.power_menu.show_shutdown();
                            forge.request_redraw();
                        }
                        2 => {
                            forge.power_menu.show_reboot();
                            forge.request_redraw();
                        }
                        _ => {}
                    },
                    SessionEvent::ClientRegistered { app_id } => {
                        info!("Session client registered: {app_id}");
                    }
                    SessionEvent::InhibitorAdded { reason } => {
                        info!("Session inhibitor added: {reason}");
                    }
                }
            }
        })
        .expect("session manager channel");

    thread::Builder::new()
        .name("forge-session-manager".into())
        .spawn(move || {
            if let Err(e) = run_dbus(sender) {
                error!("SessionManager D-Bus server failed: {e}");
            }
        })
        .expect("session manager thread");
}

fn run_dbus(sender: Sender<SessionEvent>) -> Result<(), Box<dyn std::error::Error>> {
    info!("SessionManager D-Bus server starting");

    let conn = zbus::blocking::ConnectionBuilder::session()?
        .name("org.gnome.SessionManager")?
        .serve_at(
            "/org/gnome/SessionManager",
            SessionManagerIface { sender: sender.clone() },
        )?
        .serve_at("/org/gnome/SessionManager/Presence", PresenceIface)?
        .serve_at(
            "/org/gnome/SessionManager/EndSessionDialog",
            EndSessionDialogIface { sender },
        )?
        .build()?;

    info!("SessionManager D-Bus name acquired");

    // Pre-register client paths so squeekboard etc. can find ClientPrivate
    for i in 1..=10u32 {
        let path = format!("/org/gnome/SessionManager/Client{i}");
        let obj_path = zbus::zvariant::ObjectPath::try_from(path.as_str()).unwrap();
        if let Err(e) = conn.object_server().at(obj_path, ClientPrivateIface) {
            warn!("Failed to pre-register client path: {e}");
        }
    }

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

// --- org.gnome.SessionManager ---

struct SessionManagerIface {
    sender: Sender<SessionEvent>,
}

#[zbus::interface(name = "org.gnome.SessionManager")]
impl SessionManagerIface {
    fn register_client(
        &self,
        app_id: &str,
        _client_startup_id: &str,
    ) -> zbus::fdo::Result<zbus::zvariant::OwnedObjectPath> {
        let id = NEXT_CLIENT_ID.fetch_add(1, Ordering::Relaxed);
        let path = format!("/org/gnome/SessionManager/Client{id}");
        let _ = self.sender.send(SessionEvent::ClientRegistered {
            app_id: app_id.to_string(),
        });
        Ok(zbus::zvariant::OwnedObjectPath::try_from(path).unwrap())
    }

    fn unregister_client(&self, _client_id: zbus::zvariant::ObjectPath<'_>) {}

    fn shutdown(&self) {
        let _ = self.sender.send(SessionEvent::EndSessionDialog { dialog_type: 1 });
    }

    fn reboot(&self) {
        let _ = self.sender.send(SessionEvent::EndSessionDialog { dialog_type: 2 });
    }

    fn logout(&self, _mode: u32) {
        let _ = self.sender.send(SessionEvent::EndSessionDialog { dialog_type: 0 });
    }

    fn is_session_running(&self) -> bool {
        true
    }

    fn inhibit(&self, _app_id: &str, _toplevel_xid: u32, reason: &str, _flags: u32) -> u32 {
        let id = NEXT_CLIENT_ID.fetch_add(1, Ordering::Relaxed);
        let _ = self.sender.send(SessionEvent::InhibitorAdded {
            reason: reason.to_string(),
        });
        id
    }

    fn uninhibit(&self, _inhibit_cookie: u32) {}

    fn is_inhibited(&self, _flags: u32) -> bool {
        false
    }

    #[zbus(property)]
    fn session_name(&self) -> String {
        "forge".to_string()
    }

    #[zbus(property)]
    fn session_is_active(&self) -> bool {
        true
    }

    #[zbus(property)]
    fn inhibited_actions(&self) -> u32 {
        0
    }
}

// --- org.gnome.SessionManager.Presence ---

struct PresenceIface;

#[zbus::interface(name = "org.gnome.SessionManager.Presence")]
impl PresenceIface {
    #[zbus(property)]
    fn status(&self) -> u32 {
        0
    }

    #[zbus(property)]
    fn set_status(&self, _status: u32) {}
}

// --- org.gnome.SessionManager.EndSessionDialog ---

struct EndSessionDialogIface {
    sender: Sender<SessionEvent>,
}

#[zbus::interface(name = "org.gnome.SessionManager.EndSessionDialog")]
impl EndSessionDialogIface {
    fn open(
        &self,
        dialog_type: u32,
        _seconds_to_stay_open: u32,
        _inhibitor_object_paths: Vec<zbus::zvariant::OwnedObjectPath>,
    ) {
        let _ = self.sender.send(SessionEvent::EndSessionDialog { dialog_type });
    }

    fn close(&self) {}
}

// --- org.gnome.SessionManager.ClientPrivate ---

struct ClientPrivateIface;

#[zbus::interface(name = "org.gnome.SessionManager.ClientPrivate")]
impl ClientPrivateIface {
    fn end_session_response(&self, _is_ok: bool, _reason: &str) {}

    #[zbus(signal)]
    async fn stop(ctx: &zbus::SignalContext<'_>) -> zbus::Result<()>;

    #[zbus(signal)]
    async fn query_end_session(ctx: &zbus::SignalContext<'_>, _flags: u32) -> zbus::Result<()>;

    #[zbus(signal)]
    async fn end_session(ctx: &zbus::SignalContext<'_>, _flags: u32) -> zbus::Result<()>;

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