//! Client of powermanagerd (`net.liberux.PowerManager`, system bus): forge
//! reports activity, inhibitors and screen/lock state, and applies the
//! `ScreenPower` decisions the daemon takes.

use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
use std::sync::mpsc as std_mpsc;
use std::sync::OnceLock;
use std::thread;
use std::time::Instant;

use calloop::channel::{self, Sender};
use calloop::LoopHandle;
use tracing::{error, info, warn};
use zbus::blocking::{Connection, MessageIterator, Proxy};

use crate::confirm::{ConfirmKind, ConfirmRequest};
use crate::forge::Forge;

const NAME: &str = "net.liberux.PowerManager";
const PATH: &str = "/net/liberux/PowerManager";

/// Minimum spacing between `Activity` calls while input flows.
const ACTIVITY_INTERVAL_MS: u64 = 5000;

enum Request {
    Activity,
    SetInhibited { idle: bool, suspend: bool },
    SetSessionState { screen_on: bool, locked: bool },
    SetFormFactor(&'static str),
    PostponeSuspend,
    RequestSuspend,
}

pub enum PmEvent {
    ScreenPower(bool),
    MaintenanceWindow(bool),
    BatteryWarning(u32),
    BatteryCritical { percent: u32, seconds: u32 },
    BatteryCriticalCancelled,
}

static REQUESTS: OnceLock<std_mpsc::Sender<Request>> = OnceLock::new();
static AVAILABLE: AtomicBool = AtomicBool::new(false);
static BOOT: OnceLock<Instant> = OnceLock::new();
static LAST_ACTIVITY_MS: AtomicU64 = AtomicU64::new(0);
/// Last reported (idle, suspend) inhibit bits, 0xFF until first report.
static LAST_INHIBITED: AtomicU8 = AtomicU8::new(0xFF);
/// Last reported (screen_on, locked) bits, 0xFF until first report.
static LAST_SESSION: AtomicU8 = AtomicU8::new(0xFF);
/// Last reported form factor: 0 unset, 1 desktop, 2 mobile.
static LAST_FORM: AtomicU8 = AtomicU8::new(0);

/// True once powermanagerd has answered a call; forge keeps its own idle
/// fallbacks (lock on wake) while it is false.
pub fn available() -> bool {
    AVAILABLE.load(Ordering::Relaxed)
}

pub fn activity() {
    let now = BOOT.get_or_init(Instant::now).elapsed().as_millis() as u64;
    let last = LAST_ACTIVITY_MS.load(Ordering::Relaxed);
    if now.saturating_sub(last) < ACTIVITY_INTERVAL_MS && last != 0 {
        return;
    }
    LAST_ACTIVITY_MS.store(now, Ordering::Relaxed);
    send(Request::Activity);
}

pub fn set_inhibited(idle: bool, suspend: bool) {
    let bits = (idle as u8) | ((suspend as u8) << 1);
    if LAST_INHIBITED.swap(bits, Ordering::Relaxed) != bits {
        send(Request::SetInhibited { idle, suspend });
    }
}

pub fn set_session_state(screen_on: bool, locked: bool) {
    let bits = (screen_on as u8) | ((locked as u8) << 1);
    if LAST_SESSION.swap(bits, Ordering::Relaxed) != bits {
        send(Request::SetSessionState { screen_on, locked });
    }
}

pub fn set_form_factor(mobile: bool) {
    let code = if mobile { 2 } else { 1 };
    if LAST_FORM.swap(code, Ordering::Relaxed) != code {
        send(Request::SetFormFactor(if mobile { "mobile" } else { "desktop" }));
    }
}

pub fn postpone_suspend() {
    send(Request::PostponeSuspend);
}

pub fn request_suspend() {
    send(Request::RequestSuspend);
}

fn send(request: Request) {
    if let Some(tx) = REQUESTS.get() {
        let _ = tx.send(request);
    }
}

pub fn start(event_loop: &LoopHandle<'static, Forge>) {
    let (req_tx, req_rx) = std_mpsc::channel::<Request>();
    let _ = REQUESTS.set(req_tx);

    let (ev_tx, ev_rx) = channel::channel::<PmEvent>();
    event_loop
        .insert_source(ev_rx, |event, _, forge| {
            if let channel::Event::Msg(ev) = event {
                forge.handle_powermanager_event(ev);
            }
        })
        .expect("powermanager channel");

    thread::Builder::new()
        .name("forge-powermanager".into())
        .spawn(move || {
            if let Err(e) = run_client(req_rx, ev_tx) {
                error!("powermanager client stopped: {e}");
            }
        })
        .expect("powermanager thread");
}

fn run_client(
    req_rx: std_mpsc::Receiver<Request>,
    ev_tx: Sender<PmEvent>,
) -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::system()?;
    spawn_signal_listener(conn.clone(), ev_tx)?;

    let proxy = Proxy::new(&conn, NAME, PATH, NAME)?;
    for request in req_rx {
        let result = match request {
            Request::Activity => proxy.call::<_, _, ()>("Activity", &()),
            Request::SetInhibited { idle, suspend } => {
                proxy.call::<_, _, ()>("SetInhibited", &(idle, suspend))
            }
            Request::SetSessionState { screen_on, locked } => {
                proxy.call::<_, _, ()>("SetSessionState", &(screen_on, locked))
            }
            Request::SetFormFactor(form) => proxy.call::<_, _, ()>("SetFormFactor", &(form,)),
            Request::PostponeSuspend => proxy.call::<_, _, ()>("PostponeSuspend", &()),
            Request::RequestSuspend => proxy.call::<_, _, ()>("RequestSuspend", &()),
        };
        match result {
            Ok(()) => {
                if !AVAILABLE.swap(true, Ordering::Relaxed) {
                    info!("powermanagerd is available");
                }
            }
            Err(e) => {
                if AVAILABLE.swap(false, Ordering::Relaxed) {
                    warn!("powermanagerd unavailable: {e}");
                }
            }
        }
    }
    Ok(())
}

fn spawn_signal_listener(conn: Connection, ev_tx: Sender<PmEvent>) -> zbus::Result<()> {
    let rule = zbus::MatchRule::builder()
        .msg_type(zbus::message::Type::Signal)
        .interface(NAME)?
        .path(PATH)?
        .build();
    let iter = MessageIterator::for_match_rule(rule, &conn, Some(16))?;

    thread::Builder::new()
        .name("forge-powermanager-signals".into())
        .spawn(move || {
            for msg in iter {
                let Ok(msg) = msg else { continue };
                let header = msg.header();
                let Some(member) = header.member() else { continue };
                let body = msg.body();
                let event = match member.as_str() {
                    "ScreenPower" => match body.deserialize::<(bool,)>() {
                        Ok((on,)) => PmEvent::ScreenPower(on),
                        Err(_) => continue,
                    },
                    "MaintenanceWindow" => match body.deserialize::<(bool,)>() {
                        Ok((open,)) => PmEvent::MaintenanceWindow(open),
                        Err(_) => continue,
                    },
                    "BatteryWarning" => match body.deserialize::<(u32,)>() {
                        Ok((percent,)) => PmEvent::BatteryWarning(percent),
                        Err(_) => continue,
                    },
                    "BatteryCritical" => match body.deserialize::<(u32, u32)>() {
                        Ok((percent, seconds)) => PmEvent::BatteryCritical { percent, seconds },
                        Err(_) => continue,
                    },
                    "BatteryCriticalCancelled" => PmEvent::BatteryCriticalCancelled,
                    _ => continue,
                };
                if ev_tx.send(event).is_err() {
                    break;
                }
            }
        })
        .expect("powermanager signal thread");
    Ok(())
}

impl Forge {
    pub fn handle_powermanager_event(&mut self, event: PmEvent) {
        match event {
            PmEvent::ScreenPower(on) => {
                info!("powermanager: screen power {on}");
                self.set_display_status(on);
                if on {
                    self.force_redraw(true);
                }
            }
            PmEvent::MaintenanceWindow(open) => {
                info!("powermanager: maintenance window open={open}");
            }
            PmEvent::BatteryWarning(percent) => {
                info!("powermanager: battery warning at {percent}%");
                self.show_notice(ConfirmRequest::notice(
                    "Battery Low",
                    format!("{percent}% remaining. Connect the charger."),
                ));
            }
            PmEvent::BatteryCritical { percent, seconds } => {
                info!("powermanager: battery critical at {percent}%, suspend in {seconds} s");
                self.windows.confirm.remove_if(|k| matches!(k, ConfirmKind::BatteryCritical));
                self.show_notice(ConfirmRequest {
                    title: "Battery Critical".into(),
                    message: format!("{percent}% left: suspending in {seconds} s without a charger."),
                    options: Vec::new(),
                    kind: ConfirmKind::BatteryCritical,
                    allow_label: "Postpone".into(),
                    deny_label: Some("Suspend now".into()),
                });
            }
            PmEvent::BatteryCriticalCancelled => {
                info!("powermanager: battery suspend cancelled");
                self.windows.confirm.remove_if(|k| matches!(k, ConfirmKind::BatteryCritical));
                self.sync_modal_focus();
                self.windows.set_dirty();
                self.request_redraw();
            }
        }
    }

    pub(crate) fn show_power_off_dialog(&mut self) {
        self.windows.confirm.remove_if(|k| matches!(k, ConfirmKind::PowerOff));
        self.show_notice(ConfirmRequest::power_off());
    }

    fn show_notice(&mut self, req: ConfirmRequest) {
        if !self.session.display_on {
            self.set_display_status(true);
        }
        self.windows.confirm.push(req);
        self.sync_modal_focus();
        self.windows.set_dirty();
        self.request_redraw();
    }
}
