use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};

use calloop::channel::Sender;
use calloop::LoopHandle;
use tracing::{error, info};
use zbus::zvariant::Value;

use crate::forge::Forge;
use crate::screenshot::paths;

/// Events sent from the D-Bus thread to the main event loop.
#[derive(Debug)]
pub enum GnomeShellEvent {
    /// ShowOSD request: label, icon_name, percent (0.0-1.0)
    ShowOsd {
        label: String,
        level: f64,
    },
    /// Grab accelerator request (the grab is stubbed; args are ignored).
    GrabAccelerator,
    UngrabAccelerator,
}

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

/// Start the GNOME Shell D-Bus compatibility server.
pub fn start_server(event_loop: &LoopHandle<'static, Forge>) {
    crate::services::dbus_worker::spawn_adapter(
        event_loop,
        "forge-gnome-shell-dbus",
        run_gnome_shell_dbus,
        |evt, forge| match evt {
            GnomeShellEvent::ShowOsd { label, level } => {
                let percent = (level * 100.0).round() as u8;
                let kind = if label.to_lowercase().contains("bright") {
                    crate::osd::OsdKind::Brightness
                } else {
                    crate::osd::OsdKind::Volume
                };
                forge.shell.osd.show(kind, percent, false);
                forge.request_redraw();
            }
            // Accelerator grabs are noted but not fully implemented.
            GnomeShellEvent::GrabAccelerator | GnomeShellEvent::UngrabAccelerator => {}
        },
    );
}

fn run_gnome_shell_dbus(
    sender: Sender<GnomeShellEvent>,
) -> Result<(), Box<dyn std::error::Error>> {
    info!("GNOME Shell D-Bus compatibility server starting");

    let shell_iface = ShellIface {
        sender: sender.clone(),
    };
    let screenshot_iface = ScreenshotIface {};

    let _conn = zbus::blocking::ConnectionBuilder::session()?
        .name("org.gnome.Shell")?
        .serve_at("/org/gnome/Shell", shell_iface)?
        .serve_at("/org/gnome/Shell/Screenshot", screenshot_iface)?
        .build()?;

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

// --- org.gnome.Shell interface ---

struct ShellIface {
    sender: Sender<GnomeShellEvent>,
}

#[zbus::interface(name = "org.gnome.Shell")]
impl ShellIface {
    fn show_osd(&self, params: HashMap<String, Value<'_>>) {
        let label = params
            .get("label")
            .and_then(|v| match v {
                Value::Str(s) => Some(s.as_str()),
                _ => None,
            })
            .unwrap_or("")
            .to_string();
        let level = params
            .get("level")
            .and_then(|v| match v {
                Value::F64(f) => Some(*f),
                Value::I32(i) => Some(*i as f64),
                Value::U32(u) => Some(*u as f64 / 100.0),
                _ => None,
            })
            .unwrap_or(0.0);

        let _ = self.sender.send(GnomeShellEvent::ShowOsd { label, level });
    }

    fn grab_accelerator(
        &self,
        _accelerator: String,
        _mode_flags: u32,
        _grab_flags: u32,
    ) -> u32 {
        let id = NEXT_GRAB_ID.fetch_add(1, Ordering::Relaxed);
        let _ = self.sender.send(GnomeShellEvent::GrabAccelerator);
        id
    }

    fn ungrab_accelerator(&self, _action: u32) -> bool {
        let _ = self.sender.send(GnomeShellEvent::UngrabAccelerator);
        true
    }

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

    #[zbus(property)]
    fn overview_active(&self) -> bool {
        false
    }

    #[zbus(property)]
    fn set_overview_active(&self, _active: bool) {
        // No-op
    }
}

// --- org.gnome.Shell.Screenshot interface ---

struct ScreenshotIface {}

#[zbus::interface(name = "org.gnome.Shell.Screenshot")]
impl ScreenshotIface {
    fn screenshot(
        &self,
        _include_cursor: bool,
        _flash: bool,
        filename: String,
    ) -> (bool, String) {
        match resolve_dest(&filename) {
            Some(dest) => capture(&dest, None),
            None => (false, String::new()),
        }
    }

    fn screenshot_window(
        &self,
        _include_frame: bool,
        include_cursor: bool,
        flash: bool,
        filename: String,
    ) -> (bool, String) {
        // No window-specific support: capture the full screen.
        self.screenshot(include_cursor, flash, filename)
    }

    fn screenshot_area(
        &self,
        x: i32,
        y: i32,
        width: i32,
        height: i32,
        _flash: bool,
        filename: String,
    ) -> (bool, String) {
        if width <= 0 || height <= 0 || width > 16384 || height > 16384 {
            return (false, String::new());
        }
        match resolve_dest(&filename) {
            Some(dest) => capture(&dest, Some((x, y, width, height))),
            None => (false, String::new()),
        }
    }
}

/// Resolve a screenshot destination: an internally generated path in
/// `$HOME/Screenshots` when `filename` is empty, otherwise a validated
/// caller-supplied path.
fn resolve_dest(filename: &str) -> Option<PathBuf> {
    if filename.is_empty() {
        paths::default_screenshot_path()
    } else {
        paths::validate_target(filename)
    }
}

/// Capture with `grim` to a private tempfile in the destination directory,
/// then atomically rename into place. Runs synchronously, so a truthful
/// success is only returned once the file exists.
fn capture(dest: &std::path::Path, area: Option<(i32, i32, i32, i32)>) -> (bool, String) {
    let Some(parent) = dest.parent() else {
        return (false, String::new());
    };
    let tmp = match tempfile::Builder::new().suffix(".png").tempfile_in(parent) {
        Ok(t) => t,
        Err(e) => {
            error!("screenshot tempfile failed: {e}");
            return (false, String::new());
        }
    };

    let mut cmd = Command::new("grim");
    if let Some((x, y, w, h)) = area {
        cmd.args(["-g", &format!("{x},{y} {w}x{h}")]);
    }
    cmd.arg(tmp.path());

    let ok = cmd.status().map(|s| s.success()).unwrap_or(false);
    if !ok {
        return (false, String::new());
    }

    match tmp.persist(dest) {
        Ok(_) => (true, dest.to_string_lossy().into_owned()),
        Err(e) => {
            error!("screenshot persist failed: {e}");
            (false, String::new())
        }
    }
}
