//! Keep the display awake while a ride is live (NFR-11).
//!
//! A ride is an hour with both hands on the bars and no keyboard or mouse
//! activity at all, so every desktop's idle timer eventually blanks the screen
//! and locks the session — mid-interval, with the numbers the rider is pacing
//! against behind a lock screen.
//!
//! The inhibitor is therefore held for exactly as long as a session is live
//! (`Running` **or** `Paused` — pausing for a drink is still a ride) and
//! dropped the moment it is not. An app that suppressed the lock screen for
//! the whole time it happened to be open would be a worse citizen than the
//! screensaver it is fighting.
//!
//! Failure here is never fatal: no D-Bus, no screensaver service, a sandbox
//! that refuses the inhibit — all of it costs the rider a blanked screen, not
//! a ride.

use std::sync::Mutex;

use crate::events::RideStatus;

#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
mod imp {
    /// Mobile (G-4) has its own keep-screen-on API and no `keepawake`.
    pub struct Guard;
    pub fn acquire() -> Result<Guard, String> {
        Err("no keep-awake implementation for this platform".into())
    }
    pub fn release(_guard: Guard) {}
}

#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
mod imp {
    pub type Guard = keepawake::KeepAwake;

    pub fn acquire() -> Result<Guard, keepawake::Error> {
        // Two separate inhibits on Linux: `display` is the freedesktop
        // ScreenSaver one (blank and lock), `idle` the systemd login1 one
        // (suspend-on-idle). The second needs a system-bus call that some
        // configurations refuse, and a refusal fails the whole builder — so
        // fall back to display-only rather than lose both.
        //
        // We never ask for `sleep`. A closed lid or a pressed suspend key is
        // an instruction, not an accident, and blocking it would strand the
        // machine awake in a bag.
        let build = |idle: bool| {
            keepawake::Builder::default()
                .display(true)
                .idle(idle)
                .reason("Ride in progress")
                .app_name("BikeControl")
                .app_reverse_domain("paris.tourolle.bikecontrol")
                .create()
        };
        build(true).or_else(|e| {
            tracing::debug!(error = %e, "idle inhibitor refused; falling back to display only");
            build(false)
        })
    }

    pub fn release(guard: Guard) {
        // `keepawake`'s `Drop` unwraps its D-Bus un-inhibit call, which fails
        // if the session bus went away under us. This runs on the ride loop's
        // task, and that task must not die of a screensaver.
        let dropped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(guard)));
        if dropped.is_err() {
            tracing::warn!("releasing the display inhibitor failed; it lapses when the app exits");
        }
    }
}

struct State {
    held: Option<imp::Guard>,
    /// Set after a failed acquire so a machine without a working screensaver
    /// service does not mean a fresh attempt — and a fresh warning — four
    /// times a second for a whole ride. Cleared when the ride ends, so the
    /// next one tries again.
    unavailable: bool,
}

static STATE: Mutex<State> = Mutex::new(State {
    held: None,
    unavailable: false,
});

/// Hold the inhibitor iff a ride is live. Called every ride tick; cheap and
/// idempotent, and only ever talks to the bus on a transition.
pub fn sync(status: RideStatus) {
    set(matches!(status, RideStatus::Running | RideStatus::Paused));
}

/// Acquire or release the inhibitor. Safe to call from any thread, repeatedly.
pub fn set(active: bool) {
    // A poisoned lock here means a previous caller panicked mid-transition;
    // the guard it left behind is still valid, so carry on rather than take
    // the whole ride loop down with it.
    let mut state = STATE.lock().unwrap_or_else(|p| p.into_inner());
    if active {
        if state.held.is_some() || state.unavailable {
            return;
        }
        match imp::acquire() {
            Ok(guard) => {
                state.held = Some(guard);
                tracing::info!("display sleep inhibited for the duration of the ride");
            }
            Err(e) => {
                state.unavailable = true;
                tracing::warn!(
                    error = %e,
                    "could not inhibit the screensaver — the display may blank mid-ride"
                );
            }
        }
    } else {
        state.unavailable = false;
        if let Some(guard) = state.held.take() {
            imp::release(guard);
            tracing::info!("display sleep inhibitor released");
        }
    }
}
