use std::path::PathBuf;
use std::process::Command;
use std::fs;
use std::io::Write;

pub struct VolumeState {
    pub volume: u32,
    pub muted: bool,
    pub lock_path: PathBuf,
}

// Lock file format: "{pid}:{volume}:{muted}\n"
// Written atomically on acquire; persists volume state so it survives SIGKILL.
fn lock_content(pid: u32, volume: u32, muted: bool) -> String {
    format!("{}:{}:{}\n", pid, volume, u8::from(muted))
}

fn parse_lock_content(s: &str) -> Option<(u32, u32, bool)> {
    let mut parts = s.trim().splitn(3, ':');
    let pid: u32 = parts.next()?.parse().ok()?;
    let vol: u32 = parts.next()?.parse().ok().filter(|&v| v <= 100)?;
    let muted: bool = parts.next()?.trim() == "1";
    Some((pid, vol, muted))
}

fn pid_is_alive(pid: u32) -> bool {
    let pid_t = pid as libc::pid_t;
    if pid_t <= 0 {
        return false;
    }
    // kill(pid, 0) returns 0 if the process exists, -1 (ESRCH) if not.
    unsafe { libc::kill(pid_t, 0) == 0 }
}

/// Returns lock file path: $XDG_RUNTIME_DIR/ahfail.lock or /tmp/ahfail-{uid}.lock
pub fn lock_path() -> PathBuf {
    if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
        PathBuf::from(dir).join("ahfail.lock")
    } else {
        PathBuf::from(format!("/tmp/ahfail-{}.lock", get_uid()))
    }
}

fn get_uid() -> u32 {
    unsafe { libc::getuid() }
}

/// If primary: save current system volume/mute state, set volume to 100% unmuted.
/// Returns Some(VolumeState) if this process is the primary (should restore on exit).
///
/// Uses a PID-based lock file that persists the saved volume state. If a stale lock
/// exists (holder process dead), recovers the saved volume from the file — preventing
/// permanent volume loss after SIGKILL — and takes ownership.
pub fn save_and_set_max() -> Option<VolumeState> {
    let path = lock_path();
    let our_pid = std::process::id();

    // Try atomic create (O_CREAT | O_EXCL — race-free on POSIX local filesystems).
    if fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&path)
        .map(|mut f| { let _ = f.write_all(b"0:100:0\n"); })
        .is_ok()
    {
        // We are primary. Read current volume, overwrite placeholder, set max.
        let (volume, muted) = get_current_volume();
        let _ = fs::write(&path, lock_content(our_pid, volume, muted));
        set_volume_max();
        return Some(VolumeState { volume, muted, lock_path: path });
    }

    // File exists — check if the holder PID is still alive.
    if let Ok(existing) = fs::read_to_string(&path) {
        if let Some((holder_pid, saved_vol, saved_muted)) = parse_lock_content(&existing) {
            if !pid_is_alive(holder_pid) {
                // Stale lock: recover saved volume state and take ownership.
                // The previous holder already set volume to max, so we skip set_volume_max().
                let content = lock_content(our_pid, saved_vol, saved_muted);
                if fs::write(&path, content).is_ok() {
                    return Some(VolumeState { volume: saved_vol, muted: saved_muted, lock_path: path });
                }
            }
        }
    }

    None
}

/// Restores volume from saved state and removes lock file.
pub fn restore(state: VolumeState) {
    restore_volume(state.volume, state.muted);
    let _ = fs::remove_file(&state.lock_path);
}

#[cfg(target_os = "linux")]
fn get_current_volume() -> (u32, bool) {
    let vol = Command::new("pactl")
        .args(["get-sink-volume", "@DEFAULT_SINK@"])
        .output()
        .ok()
        .and_then(|o| parse_pactl_volume(&String::from_utf8_lossy(&o.stdout)))
        .unwrap_or(100);

    let muted = Command::new("pactl")
        .args(["get-sink-mute", "@DEFAULT_SINK@"])
        .output()
        .ok()
        .map(|o| String::from_utf8_lossy(&o.stdout).contains("yes"))
        .unwrap_or(false);

    (vol, muted)
}

#[cfg(target_os = "linux")]
fn parse_pactl_volume(output: &str) -> Option<u32> {
    for token in output.split_whitespace() {
        if let Some(pct) = token.strip_suffix('%') {
            if let Ok(v) = pct.parse::<u32>() {
                if v <= 100 {
                    return Some(v);
                }
            }
        }
    }
    None
}

#[cfg(target_os = "linux")]
fn set_volume_max() {
    let _ = Command::new("pactl").args(["set-sink-mute", "@DEFAULT_SINK@", "0"]).status();
    let _ = Command::new("pactl").args(["set-sink-volume", "@DEFAULT_SINK@", "65536"]).status();
}

#[cfg(target_os = "linux")]
fn restore_volume(volume: u32, muted: bool) {
    let v = ((volume as u64 * 65536) / 100) as u32;
    let _ = Command::new("pactl")
        .args(["set-sink-volume", "@DEFAULT_SINK@", &v.to_string()])
        .status();
    let mute_arg = if muted { "1" } else { "0" };
    let _ = Command::new("pactl").args(["set-sink-mute", "@DEFAULT_SINK@", mute_arg]).status();
}

#[cfg(target_os = "macos")]
fn get_current_volume() -> (u32, bool) {
    let output = Command::new("osascript")
        .args(["-e", "output volume of (get volume settings)"])
        .output()
        .ok();
    let vol = output.as_ref()
        .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse().ok())
        .unwrap_or(100);

    let muted = Command::new("osascript")
        .args(["-e", "output muted of (get volume settings)"])
        .output()
        .ok()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "true")
        .unwrap_or(false);

    (vol, muted)
}

#[cfg(target_os = "macos")]
fn set_volume_max() {
    let _ = Command::new("osascript")
        .args(["-e", "set volume output volume 100 without output muted"])
        .status();
}

#[cfg(target_os = "macos")]
fn restore_volume(volume: u32, muted: bool) {
    let script = if muted {
        format!("set volume output volume {} with output muted", volume)
    } else {
        format!("set volume output volume {} without output muted", volume)
    };
    let _ = Command::new("osascript").args(["-e", &script]).status();
}

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn get_current_volume() -> (u32, bool) { (100, false) }

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn set_volume_max() {}

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn restore_volume(_volume: u32, _muted: bool) {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_lock_content_roundtrip() {
        let content = lock_content(1234, 75, true);
        let (pid, vol, muted) = parse_lock_content(&content).unwrap();
        assert_eq!(pid, 1234);
        assert_eq!(vol, 75);
        assert!(muted);
    }

    #[test]
    fn parse_lock_content_rejects_invalid_volume() {
        assert!(parse_lock_content("123:101:0").is_none());
        assert!(parse_lock_content("123:abc:0").is_none());
    }

    #[test]
    fn pid_is_alive_current_process() {
        assert!(pid_is_alive(std::process::id()));
    }

    #[test]
    fn pid_is_alive_zero_is_not_treated_as_alive() {
        // PID 0 has special kill() semantics; we guard against it.
        assert!(!pid_is_alive(0));
    }

    #[test]
    fn fresh_lock_creates_file_and_stale_is_recovered() {
        let dir = tempfile::tempdir().unwrap();
        let lock = dir.path().join("ahfail.lock");

        // Write a stale lock file: PID 99_999_999 is above Linux's max_pid (4_194_304)
        // and will always fail kill(pid, 0) with ESRCH.
        std::fs::write(&lock, "99999999:60:1\n").unwrap();

        // Simulate save_and_set_max() stale-recovery path directly.
        let our_pid = std::process::id();
        if let Ok(existing) = std::fs::read_to_string(&lock) {
            if let Some((holder_pid, saved_vol, saved_muted)) = parse_lock_content(&existing) {
                assert!(!pid_is_alive(holder_pid), "test PID should not be alive");
                let content = lock_content(our_pid, saved_vol, saved_muted);
                std::fs::write(&lock, content).unwrap();
                let (pid, vol, muted) = parse_lock_content(&std::fs::read_to_string(&lock).unwrap()).unwrap();
                assert_eq!(pid, our_pid);
                assert_eq!(vol, 60);
                assert!(muted);
            } else {
                panic!("parse_lock_content failed");
            }
        }
    }

    #[test]
    fn second_acquisition_blocked_by_alive_pid() {
        let dir = tempfile::tempdir().unwrap();
        let lock = dir.path().join("ahfail.lock");

        // Write a lock file held by our own (alive) PID.
        let our_pid = std::process::id();
        std::fs::write(&lock, lock_content(our_pid, 80, false)).unwrap();

        // Attempt to acquire — should be blocked because holder is alive.
        if let Ok(existing) = std::fs::read_to_string(&lock) {
            if let Some((holder_pid, _, _)) = parse_lock_content(&existing) {
                assert!(pid_is_alive(holder_pid), "our own PID should be alive");
            }
        }
    }
}
