//! org.freedesktop.login1 (logind) D-Bus integration.
//!
//! Handles session management: lock/unlock signals, the suspend delay
//! inhibitor, and idle hints.

use std::os::fd::OwnedFd;
use std::sync::mpsc as std_mpsc;
use std::sync::Mutex;
use std::thread;

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

use crate::forge::Forge;

/// Events sent from the logind listener to the main event loop.
#[derive(Debug)]
pub enum LogindEvent {
    /// Session locked externally (e.g., loginctl lock-session).
    Lock,
    /// Session unlocked externally.
    Unlock,
    /// System is about to suspend — prepare.
    PrepareForSleep(bool),
    /// The `handle-power-key` block inhibitor, held for the session's lifetime.
    PowerKeyInhibitor(OwnedFd),
}

static SENDER: Mutex<Option<Sender<LogindEvent>>> = Mutex::new(None);

/// Sent back to the listener thread once the compositor has finished
/// preparing for sleep, so it can drop the delay inhibitor and let
/// logind proceed.
pub struct SleepReady;

/// Returns true if the current logind session type is "greeter".
pub fn is_greeter_session() -> bool {
    let Ok(conn) = zbus::blocking::Connection::system() else { return false };
    let session_id = std::env::var("XDG_SESSION_ID").unwrap_or_else(|_| "auto".into());
    let Ok(reply) = conn.call_method(
        Some("org.freedesktop.login1"),
        "/org/freedesktop/login1",
        Some("org.freedesktop.login1.Manager"),
        "GetSession",
        &(&session_id,),
    ) else { return false };
    let Ok(session_path) = reply.body().deserialize::<zbus::zvariant::OwnedObjectPath>() else {
        return false;
    };
    let Ok(reply) = conn.call_method(
        Some("org.freedesktop.login1"),
        session_path.as_str(),
        Some("org.freedesktop.DBus.Properties"),
        "Get",
        &("org.freedesktop.login1.Session", "Class"),
    ) else { return false };
    let Ok((value,)) = reply.body().deserialize::<(zbus::zvariant::OwnedValue,)>() else {
        return false;
    };
    String::try_from(value).as_deref() == Ok("greeter")
}

/// Start the logind D-Bus listener.
pub fn start_listener(event_loop: &LoopHandle<'static, Forge>) {
    let (sender, channel) = channel::channel::<LogindEvent>();
    *SENDER.lock().unwrap() = Some(sender.clone());
    // Handshake back to the listener: PrepareForSleep(true) is handled
    // on the main loop, and only when that returns may the inhibitor be
    // released. Without it logind starts freezing the machine while the
    // lock surface is still being spawned, and the user can come back
    // to an unlocked screen.
    let (ready_tx, ready_rx) = std_mpsc::channel::<SleepReady>();

    event_loop
        .insert_source(channel, move |event, _, forge| {
            if let calloop::channel::Event::Msg(ev) = event {
                match ev {
                    LogindEvent::Lock => {
                        info!("logind: lock requested");
                        forge.lock_session();
                        forge.request_redraw();
                    }
                    LogindEvent::Unlock => {
                        info!("logind: unlock requested");
                    }
                    LogindEvent::PrepareForSleep(start) => {
                        if start {
                            info!("logind: preparing for sleep");
                            forge.session.wake_display = forge.session.display_on;
                            forge.lock_session();
                            forge.set_display_status(false);
                            let _ = ready_tx.send(SleepReady);
                        } else {
                            info!("logind: resumed from sleep");
                            if forge.session.wake_display {
                                forge.set_display_status(true);
                            } else {
                                // The backlight controller powers back up on resume; a
                                // maintenance wake would otherwise light the panel for the
                                // whole window. Re-assert the blank the hardware forgot.
                                crate::backlight::set_power(false);
                            }
                            // Outputs can come back changed (docked,
                            // undocked, different mode); re-probe rather
                            // than trusting the pre-suspend state.
                            forge.reprobe_outputs();
                            forge.force_redraw(true);
                        }
                    }
                    LogindEvent::PowerKeyInhibitor(fd) => {
                        info!("logind: holding the power key inhibitor");
                        forge.session.power_key_inhibitor = Some(fd);
                    }
                }
            }
        })
        .expect("logind D-Bus channel");

    thread::Builder::new()
        .name("forge-logind".into())
        .spawn(move || {
            if let Err(e) = run_logind_listener(sender, ready_rx) {
                error!("logind D-Bus listener failed: {e}");
            }
        })
        .expect("logind thread");
}

fn inhibit(conn: &zbus::blocking::Connection, what: &str, why: &str, mode: &str) -> Option<OwnedFd> {
    let reply = conn
        .call_method(
            Some("org.freedesktop.login1"),
            "/org/freedesktop/login1",
            Some("org.freedesktop.login1.Manager"),
            "Inhibit",
            &(what, "forge", why, mode),
        )
        .map_err(|e| warn!("logind: Inhibit({what}) failed: {e}"))
        .ok()?;
    reply
        .body()
        .deserialize::<zbus::zvariant::OwnedFd>()
        .map_err(|e| warn!("logind: Inhibit returned no fd: {e}"))
        .ok()
        .map(OwnedFd::from)
}

/// Take a `delay` inhibitor so logind waits for us (up to
/// `InhibitDelayMaxSec`, 5 s by default) before suspending.
fn take_sleep_inhibitor(conn: &zbus::blocking::Connection) -> Option<OwnedFd> {
    inhibit(conn, "sleep", "Locking the session before sleep", "delay")
}

/// Keep logind from acting on the power key itself; the compositor handles it.
/// polkit grants this to active sessions only, so call it once the session is.
pub fn inhibit_power_key() {
    let Some(sender) = SENDER.lock().unwrap().clone() else { return };
    thread::spawn(move || {
        let Ok(conn) = zbus::blocking::Connection::system() else { return };
        if let Some(fd) = inhibit(&conn, "handle-power-key", "The compositor handles the power key", "block") {
            let _ = sender.send(LogindEvent::PowerKeyInhibitor(fd));
        }
    });
}

pub fn power_off() {
    thread::spawn(|| {
        let Ok(conn) = zbus::blocking::Connection::system() else { return };
        if let Err(e) = conn.call_method(
            Some("org.freedesktop.login1"),
            "/org/freedesktop/login1",
            Some("org.freedesktop.login1.Manager"),
            "PowerOff",
            &(false,),
        ) {
            error!("logind: PowerOff failed: {e}");
        }
    });
}

fn run_logind_listener(
    sender: Sender<LogindEvent>,
    ready_rx: std_mpsc::Receiver<SleepReady>,
) -> Result<(), Box<dyn std::error::Error>> {
    info!("logind D-Bus listener starting");

    let conn = zbus::blocking::Connection::system()?;

    let mut inhibitor = take_sleep_inhibitor(&conn);
    if inhibitor.is_some() {
        info!("logind: holding a sleep delay inhibitor");
    }

    // Get the session path for the current session
    let session_id = std::env::var("XDG_SESSION_ID").unwrap_or_else(|_| "auto".into());
    let reply = conn.call_method(
        Some("org.freedesktop.login1"),
        "/org/freedesktop/login1",
        Some("org.freedesktop.login1.Manager"),
        "GetSession",
        &(&session_id,),
    )?;
    let session_path: zbus::zvariant::OwnedObjectPath = reply.body().deserialize()?;

    info!("logind session: {}", session_path.as_str());

    // Also set idle hint so logind knows we're managing idle
    let _ = conn.call_method(
        Some("org.freedesktop.login1"),
        session_path.as_str(),
        Some("org.freedesktop.login1.Session"),
        "SetIdleHint",
        &(false,),
    );

    // Build a match rule that captures all logind signals we care about
    let rule = zbus::MatchRule::builder()
        .msg_type(zbus::message::Type::Signal)
        .sender("org.freedesktop.login1")?
        .build();

    let mut iter =
        zbus::blocking::MessageIterator::for_match_rule(rule, &conn, Some(16))?;

    while let Some(Ok(msg)) = iter.next() {
        let header = msg.header();
        let member = match header.member() {
            Some(m) => m,
            None => continue,
        };
        let path = header.path().map(|p| p.as_str().to_owned()).unwrap_or_default();

        match member.as_str() {
            "Lock" if path == session_path.as_str() => {
                let _ = sender.send(LogindEvent::Lock);
            }
            "Unlock" if path == session_path.as_str() => {
                let _ = sender.send(LogindEvent::Unlock);
            }
            "PrepareForSleep" => {
                if let Ok((start,)) = msg.body().deserialize::<(bool,)>() {
                    let _ = sender.send(LogindEvent::PrepareForSleep(start));
                    if start {
                        // Block until the main loop reports the session
                        // locked and the display off, then release the
                        // inhibitor so the suspend proceeds. The wait is
                        // bounded well inside InhibitDelayMaxSec so a
                        // wedged compositor cannot veto sleep.
                        match ready_rx.recv_timeout(std::time::Duration::from_secs(3)) {
                            Ok(SleepReady) => info!("logind: prepared, releasing inhibitor"),
                            Err(e) => warn!("logind: sleep prepare did not confirm ({e}), releasing inhibitor"),
                        }
                        drop(inhibitor.take());
                    } else {
                        // Re-arm for the next cycle.
                        inhibitor = take_sleep_inhibitor(&conn);
                    }
                }
            }
            _ => {}
        }
    }

    Ok(())
}
