use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use std::process::Command;

#[derive(Debug, Serialize, Clone)]
pub struct WindowInfo {
    pub exe_name: String,
    pub exe_path: String,
    pub title: String,
    pub display_name: String,
    pub icon: Option<String>,
}

/// Get system idle time in seconds via D-Bus.
///
/// Tries (in order):
/// 1. org.gnome.Mutter.IdleMonitor.GetIdletime  (returns milliseconds)
/// 2. org.freedesktop.ScreenSaver.GetSessionIdleTime (returns seconds)
/// 3. Falls back to 0
pub fn get_system_idle_seconds() -> u64 {
    // Try GNOME Mutter IdleMonitor (returns milliseconds)
    if let Some(ms) = gdbus_call_u64(
        "org.gnome.Mutter.IdleMonitor",
        "/org/gnome/Mutter/IdleMonitor/Core",
        "org.gnome.Mutter.IdleMonitor",
        "GetIdletime",
    ) {
        return ms / 1000;
    }

    // Try freedesktop ScreenSaver (returns seconds)
    if let Some(secs) = gdbus_call_u64(
        "org.freedesktop.ScreenSaver",
        "/org/freedesktop/ScreenSaver",
        "org.freedesktop.ScreenSaver",
        "GetSessionIdleTime",
    ) {
        return secs;
    }

    0
}

/// Call a D-Bus method that returns a single uint64/uint32 value via gdbus.
/// Parses the `(uint64 12345,)` or `(uint32 12345,)` output format.
fn gdbus_call_u64(dest: &str, object: &str, interface: &str, method: &str) -> Option<u64> {
    let output = Command::new("gdbus")
        .args([
            "call",
            "--session",
            "--dest",
            dest,
            "--object-path",
            object,
            "--method",
            &format!("{}.{}", interface, method),
        ])
        .output()
        .ok()?;

    if !output.status.success() {
        return None;
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    // Format: "(uint64 12345,)" or "(uint32 12345,)"
    // Extract the number after "uint64 " or "uint32 "
    let s = stdout.trim();
    for prefix in &["uint64 ", "uint32 "] {
        if let Some(pos) = s.find(prefix) {
            let after = &s[pos + prefix.len()..];
            let num_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
            return num_str.parse().ok();
        }
    }
    None
}

/// Get the current user's UID from /proc/self/status.
fn get_current_uid() -> u32 {
    if let Ok(status) = fs::read_to_string("/proc/self/status") {
        for line in status.lines() {
            if let Some(rest) = line.strip_prefix("Uid:") {
                if let Some(uid_str) = rest.split_whitespace().next() {
                    if let Ok(uid) = uid_str.parse::<u32>() {
                        return uid;
                    }
                }
            }
        }
    }
    u32::MAX // Unlikely fallback  - won't match any process
}

/// Enumerate running processes from /proc.
///
/// Reads /proc/[pid]/exe, /proc/[pid]/comm, and /proc/[pid]/status to build
/// a list of user-space processes. Filters out kernel threads, zombies, and
/// common system daemons. Deduplicates by exe path.
pub fn enumerate_running_processes() -> Vec<WindowInfo> {
    let my_uid = get_current_uid();
    let mut seen_paths = HashSet::new();
    let mut results = Vec::new();

    let Ok(entries) = fs::read_dir("/proc") else {
        return results;
    };

    for entry in entries.flatten() {
        let name = entry.file_name();
        let name_str = name.to_string_lossy();

        // Only numeric directories (PIDs)
        if !name_str.chars().all(|c| c.is_ascii_digit()) {
            continue;
        }

        let pid_path = entry.path();

        // Check process belongs to current user
        if let Ok(status) = fs::read_to_string(pid_path.join("status")) {
            let mut is_our_uid = false;
            let mut ppid: u32 = 1;

            for line in status.lines() {
                if let Some(rest) = line.strip_prefix("Uid:") {
                    if let Some(uid_str) = rest.split_whitespace().next() {
                        if let Ok(uid) = uid_str.parse::<u32>() {
                            is_our_uid = uid == my_uid;
                        }
                    }
                }
                if let Some(rest) = line.strip_prefix("PPid:") {
                    if let Ok(p) = rest.trim().parse::<u32>() {
                        ppid = p;
                    }
                }
            }

            if !is_our_uid {
                continue;
            }

            // Skip kernel threads (ppid 0 or 2)
            if ppid == 0 || ppid == 2 {
                continue;
            }
        } else {
            continue;
        }

        // Skip zombies (empty cmdline)
        if let Ok(cmdline) = fs::read(pid_path.join("cmdline")) {
            if cmdline.is_empty() {
                continue;
            }
        }

        // Get exe path via symlink
        let exe_path = match fs::read_link(pid_path.join("exe")) {
            Ok(p) => {
                let ps = p.to_string_lossy().to_string();
                // Skip deleted executables
                if ps.contains(" (deleted)") {
                    continue;
                }
                ps
            }
            Err(_) => continue,
        };

        // Deduplicate by exe path
        if !seen_paths.insert(exe_path.clone()) {
            continue;
        }

        // Get comm (short process name)
        let comm = fs::read_to_string(pid_path.join("comm"))
            .unwrap_or_default()
            .trim()
            .to_string();

        // Skip common system daemons / background services
        if is_system_daemon(&comm, &exe_path) {
            continue;
        }

        let exe_name = std::path::Path::new(&exe_path)
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| comm.clone());

        let display_name = prettify_name(&exe_name);

        results.push(WindowInfo {
            exe_name,
            exe_path,
            title: String::new(),
            display_name,
            icon: None,
        });
    }

    // Resolve icons from .desktop files
    let icon_map = build_desktop_icon_map();
    for info in &mut results {
        if let Some(icon_name) = icon_map.get(&info.exe_name) {
            if let Some(data_url) = resolve_icon_to_data_url(icon_name) {
                info.icon = Some(data_url);
            }
        }
    }

    results.sort_by(|a, b| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()));
    results
}

/// On Linux we cannot detect window visibility (Wayland security model).
/// Return running processes filtered to likely-GUI apps as a best-effort
/// approximation  - the timer will pause only when the tracked process exits.
pub fn enumerate_visible_windows() -> Vec<WindowInfo> {
    enumerate_running_processes()
        .into_iter()
        .filter(|w| is_likely_gui_app(&w.exe_path, &w.exe_name))
        .collect()
}

/// Heuristic: returns true for processes that are likely background daemons.
fn is_system_daemon(comm: &str, exe_path: &str) -> bool {
    const DAEMON_NAMES: &[&str] = &[
        "systemd",
        "dbus-daemon",
        "dbus-broker",
        "pipewire",
        "pipewire-pulse",
        "wireplumber",
        "pulseaudio",
        "xdg-desktop-portal",
        "xdg-document-portal",
        "xdg-permission-store",
        "gvfsd",
        "gvfs-udisks2-volume-monitor",
        "at-spi-bus-launcher",
        "at-spi2-registryd",
        "gnome-keyring-daemon",
        "ssh-agent",
        "gpg-agent",
        "polkitd",
        "gsd-",
        "evolution-data-server",
        "evolution-calendar-factory",
        "evolution-addressbook-factory",
        "tracker-miner-fs",
        "tracker-extract",
        "ibus-daemon",
        "ibus-x11",
        "ibus-portal",
        "ibus-extension-gtk3",
        "fcitx5",
        "goa-daemon",
        "goa-identity-service",
        "xdg-desktop-portal-gnome",
        "xdg-desktop-portal-gtk",
        "xdg-desktop-portal-kde",
        "xdg-desktop-portal-wlr",
    ];

    // Exact match
    if DAEMON_NAMES.contains(&comm) {
        return true;
    }

    // Prefix match (e.g. gsd-*)
    for prefix in &["gsd-", "gvfs", "xdg-desktop-portal"] {
        if comm.starts_with(prefix) {
            return true;
        }
    }

    // System paths
    if exe_path.starts_with("/usr/libexec/")
        || exe_path.starts_with("/usr/lib/systemd/")
        || exe_path.starts_with("/usr/lib/polkit")
    {
        return true;
    }

    false
}

/// Heuristic: returns true for processes that are likely GUI applications.
fn is_likely_gui_app(exe_path: &str, exe_name: &str) -> bool {
    // Common GUI app locations
    let gui_paths = ["/usr/bin/", "/usr/local/bin/", "/opt/", "/snap/", "/flatpak/"];
    let in_gui_path = gui_paths.iter().any(|p| exe_path.starts_with(p))
        || exe_path.contains("/AppRun")
        || exe_path.contains(".AppImage");

    // Home directory apps (Electron, AppImage, etc.)
    let in_home = exe_path.contains("/.local/") || exe_path.contains("/home/");

    if !in_gui_path && !in_home {
        return false;
    }

    // Exclude known CLI-only tools
    const CLI_TOOLS: &[&str] = &[
        "bash", "sh", "zsh", "fish", "dash", "csh", "tcsh",
        "cat", "ls", "grep", "find", "sed", "awk", "sort", "cut",
        "curl", "wget", "ssh", "scp", "rsync",
        "git", "make", "cmake", "cargo", "npm", "node", "python", "python3",
        "ruby", "perl", "java", "javac",
        "top", "htop", "btop", "tmux", "screen",
        "sudo", "su", "pkexec",
        "journalctl", "systemctl",
    ];

    if CLI_TOOLS.contains(&exe_name) {
        return false;
    }

    true
}

/// Build a map from exe_name → icon_name by scanning .desktop files.
fn build_desktop_icon_map() -> HashMap<String, String> {
    let mut map = HashMap::new();
    let dirs = [
        "/usr/share/applications",
        "/usr/local/share/applications",
        "/var/lib/flatpak/exports/share/applications",
        "/var/lib/snapd/desktop/applications",
    ];

    // Also check ~/.local/share/applications
    let home_apps = std::env::var("HOME")
        .map(|h| format!("{h}/.local/share/applications"))
        .unwrap_or_default();

    for dir in dirs.iter().chain(std::iter::once(&home_apps.as_str())) {
        if dir.is_empty() {
            continue;
        }
        let Ok(entries) = fs::read_dir(dir) else { continue };
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("desktop") {
                continue;
            }
            if let Ok(content) = fs::read_to_string(&path) {
                parse_desktop_entry(&content, &mut map);
            }
        }
    }
    map
}

/// Parse a .desktop file and extract exe_name → icon_name mappings.
fn parse_desktop_entry(content: &str, map: &mut HashMap<String, String>) {
    let mut icon = None;
    let mut exec = None;
    let mut in_desktop_entry = false;

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_desktop_entry = trimmed == "[Desktop Entry]";
            continue;
        }
        if !in_desktop_entry {
            continue;
        }
        if let Some(val) = trimmed.strip_prefix("Icon=") {
            icon = Some(val.trim().to_string());
        } else if let Some(val) = trimmed.strip_prefix("Exec=") {
            // Extract the binary name from the Exec line
            // Exec can be: /usr/bin/foo, env VAR=val foo, flatpak run ..., etc.
            let val = val.trim();
            // Strip env prefix
            let cmd_part = if val.starts_with("env ") {
                // Skip env KEY=VAL pairs
                val.split_whitespace()
                    .skip(1)
                    .find(|s| !s.contains('='))
                    .unwrap_or("")
            } else {
                val.split_whitespace().next().unwrap_or("")
            };
            // Get just the filename
            if let Some(name) = Path::new(cmd_part).file_name() {
                exec = Some(name.to_string_lossy().to_string());
            }
        }
    }

    if let (Some(exe_name), Some(icon_name)) = (exec, icon) {
        if !exe_name.is_empty() && !icon_name.is_empty() {
            map.entry(exe_name).or_insert(icon_name);
        }
    }
}

/// Resolve an icon name to a base64 data URL.
/// Handles both absolute paths and theme icon names.
fn resolve_icon_to_data_url(icon_name: &str) -> Option<String> {
    // If it's an absolute path, read directly
    if icon_name.starts_with('/') {
        return read_icon_file(Path::new(icon_name));
    }

    // Search hicolor theme at standard sizes (prefer smaller for 20x20 display)
    let sizes = ["32x32", "48x48", "24x24", "64x64", "scalable", "128x128", "256x256"];
    let theme_dirs = [
        "/usr/share/icons/hicolor",
        "/usr/share/pixmaps",
    ];

    // Also check user theme
    let home_icons = std::env::var("HOME")
        .map(|h| format!("{h}/.local/share/icons/hicolor"))
        .unwrap_or_default();

    // Try hicolor theme with size variants
    for base in theme_dirs.iter().map(|s| s.to_string()).chain(
        if home_icons.is_empty() { None } else { Some(home_icons.clone()) }
    ) {
        if base.ends_with("pixmaps") {
            // /usr/share/pixmaps has flat layout
            for ext in &["png", "svg", "xpm"] {
                let path = format!("{base}/{icon_name}.{ext}");
                if let Some(url) = read_icon_file(Path::new(&path)) {
                    return Some(url);
                }
            }
        } else {
            for size in &sizes {
                for ext in &["png", "svg"] {
                    let path = format!("{base}/{size}/apps/{icon_name}.{ext}");
                    if let Some(url) = read_icon_file(Path::new(&path)) {
                        return Some(url);
                    }
                }
            }
        }
    }

    None
}

/// Read an icon file and return it as a base64 data URL.
/// Supports PNG and SVG.
fn read_icon_file(path: &Path) -> Option<String> {
    if !path.exists() {
        return None;
    }
    let ext = path.extension()?.to_str()?;
    let data = fs::read(path).ok()?;
    if data.is_empty() {
        return None;
    }

    let mime = match ext {
        "png" => "image/png",
        "svg" => "image/svg+xml",
        "xpm" => return None, // XPM isn't useful as a data URL
        _ => return None,
    };

    let mut b64 = String::new();
    base64_encode(&data, &mut b64);

    Some(format!("data:{mime};base64,{b64}"))
}

/// Base64 encode bytes into a string (no padding variants, standard alphabet).
fn base64_encode(input: &[u8], output: &mut String) {
    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    let mut i = 0;
    let len = input.len();
    output.reserve((len + 2) / 3 * 4);

    while i + 2 < len {
        let n = (input[i] as u32) << 16 | (input[i + 1] as u32) << 8 | input[i + 2] as u32;
        output.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char);
        output.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char);
        output.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char);
        output.push(ALPHABET[(n & 0x3F) as usize] as char);
        i += 3;
    }

    match len - i {
        1 => {
            let n = (input[i] as u32) << 16;
            output.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char);
            output.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char);
            output.push('=');
            output.push('=');
        }
        2 => {
            let n = (input[i] as u32) << 16 | (input[i + 1] as u32) << 8;
            output.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char);
            output.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char);
            output.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char);
            output.push('=');
        }
        _ => {}
    }
}

/// Convert an exe name like "gnome-terminal-server" into "Gnome Terminal Server".
fn prettify_name(name: &str) -> String {
    // Strip common suffixes
    let stripped = name
        .strip_suffix("-bin")
        .or_else(|| name.strip_suffix(".bin"))
        .unwrap_or(name);

    stripped
        .split(|c: char| c == '-' || c == '_')
        .filter(|s| !s.is_empty())
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(first) => {
                    let mut s = first.to_uppercase().to_string();
                    s.extend(chars);
                    s
                }
                None => String::new(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}
