use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{LazyLock, Mutex};

use calloop::channel::Sender;
use calloop::LoopHandle;
use tracing::info;

use crate::forge::Forge;

/// org.gnome.ScreenSaver + org.freedesktop.ScreenSaver D-Bus servers.
/// GNOME apps use the former to lock the screen; media players and
/// browsers use the latter's Inhibit/UnInhibit to keep the screen from
/// blanking or locking during video playback. The inhibition registry is
/// shared with the org.gnome.SessionManager server, whose Inhibit is
/// what Chromium-based browsers actually reach first.

static INHIBIT_COUNT: AtomicUsize = AtomicUsize::new(0);
static INHIBITS: LazyLock<Mutex<InhibitState>> =
    LazyLock::new(|| Mutex::new(InhibitState::default()));

/// D-Bus mirror of the orientation lock, so the `RotationLock` property has a
/// value to report. Defaults to locked, matching the compositor's boot state.
static ROTATION_LOCKED: AtomicBool = AtomicBool::new(true);

/// True while at least one idle-relevant inhibition is held.
pub fn inhibited() -> bool {
    INHIBIT_COUNT.load(Ordering::Relaxed) > 0
}

struct Holder {
    /// Unique bus name, so inhibitions leaked by a crashed client can be
    /// dropped on NameOwnerChanged.
    owner: String,
    /// False for session inhibits that do not involve idle (logout /
    /// switch-user): they get a valid cookie but never block the lock.
    keeps_awake: bool,
}

#[derive(Default)]
struct InhibitState {
    next_cookie: u32,
    holders: HashMap<u32, Holder>,
}

fn sync_count(st: &InhibitState) {
    let awake = st.holders.values().filter(|h| h.keeps_awake).count();
    INHIBIT_COUNT.store(awake, Ordering::Relaxed);
}

pub fn register_inhibit(owner: String, keeps_awake: bool) -> u32 {
    let mut st = INHIBITS.lock().unwrap_or_else(|e| e.into_inner());
    st.next_cookie = st.next_cookie.wrapping_add(1).max(1);
    let cookie = st.next_cookie;
    st.holders.insert(cookie, Holder { owner, keeps_awake });
    sync_count(&st);
    cookie
}

pub fn release_inhibit(cookie: u32) -> bool {
    let mut st = INHIBITS.lock().unwrap_or_else(|e| e.into_inner());
    let removed = st.holders.remove(&cookie).is_some();
    if removed {
        sync_count(&st);
    }
    removed
}

fn purge_owner(owner: &str) -> usize {
    let mut st = INHIBITS.lock().unwrap_or_else(|e| e.into_inner());
    let before = st.holders.len();
    st.holders.retain(|_, h| h.owner != owner);
    let dropped = before - st.holders.len();
    if dropped > 0 {
        sync_count(&st);
    }
    dropped
}

#[derive(Debug)]
pub enum ScreenSaverEvent {
    Lock,
    RotationLock(bool),
}

pub fn start_server(event_loop: &LoopHandle<'static, Forge>) {
    crate::services::dbus_worker::spawn_adapter(
        event_loop,
        "forge-screensaver",
        run_screensaver_dbus,
        |ev, forge| match ev {
            ScreenSaverEvent::Lock => {
                forge.lock_session();
                forge.request_redraw();
            }
            ScreenSaverEvent::RotationLock(locked) => {
                if locked {
                    forge.lock_orientation(None);
                } else {
                    forge.unlock_orientation();
                }
            }
        },
    );
}

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

    let fdo_iface = || FdoScreenSaverIface { sender: sender.clone() };

    let conn = zbus::blocking::ConnectionBuilder::session()?
        .name("org.gnome.ScreenSaver")?
        .serve_at("/org/gnome/ScreenSaver", ScreenSaverIface { sender: sender.clone() })?
        // Clients are split between the two historical object paths.
        .serve_at("/org/freedesktop/ScreenSaver", fdo_iface())?
        .serve_at("/ScreenSaver", fdo_iface())?
        .build()?;
    conn.request_name("org.freedesktop.ScreenSaver")?;

    // Drop inhibitions whose holder fell off the bus without UnInhibit.
    let dbus = zbus::blocking::fdo::DBusProxy::new(&conn)?;
    for signal in dbus.receive_name_owner_changed()? {
        let args = signal.args()?;
        if args.new_owner().is_none() {
            if let Some(old) = args.old_owner().as_ref() {
                let dropped = purge_owner(old.as_str());
                if dropped > 0 {
                    info!("ScreenSaver: dropped {dropped} inhibition(s) from vanished {old}");
                }
            }
        }
    }

    Err("NameOwnerChanged stream ended".into())
}

struct ScreenSaverIface {
    sender: Sender<ScreenSaverEvent>,
}

#[zbus::interface(name = "org.gnome.ScreenSaver")]
impl ScreenSaverIface {
    /// Lock the screen.
    fn lock(&self) {
        let _ = self.sender.send(ScreenSaverEvent::Lock);
    }

    /// Returns true if the screen is currently locked.
    fn get_active(&self) -> bool {
        // We can't easily query the main thread synchronously,
        // so return false. Apps mostly use Lock(), not GetActive().
        false
    }

    /// Simulate activity to reset idle timers.
    fn simulate_user_activity(&self) {
        // No-op; idle is handled by powermanagerd
    }

    /// Set the screensaver active state.
    fn set_active(&self, active: bool) {
        if active {
            let _ = self.sender.send(ScreenSaverEvent::Lock);
        }
    }

    /// Orientation lock, driven by the settings rotation toggle. `true` freezes
    /// the current orientation; `false` re-enables accelerometer auto-rotation.
    #[zbus(property)]
    fn rotation_lock(&self) -> bool {
        ROTATION_LOCKED.load(Ordering::Relaxed)
    }

    #[zbus(property)]
    fn set_rotation_lock(&self, locked: bool) {
        ROTATION_LOCKED.store(locked, Ordering::Relaxed);
        let _ = self.sender.send(ScreenSaverEvent::RotationLock(locked));
    }

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

struct FdoScreenSaverIface {
    sender: Sender<ScreenSaverEvent>,
}

#[zbus::interface(name = "org.freedesktop.ScreenSaver")]
impl FdoScreenSaverIface {
    fn inhibit(
        &self,
        application_name: &str,
        reason_for_inhibit: &str,
        #[zbus(header)] header: zbus::message::Header<'_>,
    ) -> u32 {
        let owner = header.sender().map(|s| s.to_string()).unwrap_or_default();
        let cookie = register_inhibit(owner, true);
        info!("ScreenSaver: inhibited by {application_name} ({reason_for_inhibit}), cookie {cookie}");
        cookie
    }

    fn un_inhibit(&self, cookie: u32) {
        if release_inhibit(cookie) {
            info!("ScreenSaver: uninhibited, cookie {cookie}");
        }
    }

    fn lock(&self) {
        let _ = self.sender.send(ScreenSaverEvent::Lock);
    }

    fn get_active(&self) -> bool {
        false
    }

    fn get_active_time(&self) -> u32 {
        0
    }

    fn get_session_idle_time(&self) -> u32 {
        0
    }

    fn set_active(&self, active: bool) {
        if active {
            let _ = self.sender.send(ScreenSaverEvent::Lock);
        }
    }

    fn simulate_user_activity(&self) {}
}
