use std::{
    cell::LazyCell,
    ffi::{OsStr, OsString},
    os::unix::{ffi::OsStrExt as _, fs::FileTypeExt as _},
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{Result, bail};
use log::{debug, trace};
use walkdir::WalkDir;

#[derive(Debug)]
pub struct Process {
    pid: u64,
    cwd: LazyCell<PathBuf, Box<dyn FnOnce() -> PathBuf>>,
}

impl Process {
    pub fn from_pid(pid: u64) -> Self {
        Self {
            pid,
            cwd: LazyCell::new(Box::new(move || {
                trace!("Running `lsof` to find working directory of pid={pid}");
                Command::new("lsof")
                    .args(["-a", "-d", "cwd", "-Fn", "-p", &pid.to_string()])
                    .output()
                    .unwrap()
                    .stdout
                    .split(|c| *c == b'\n')
                    .find_map(|l| {
                        l.strip_prefix(b"n")
                            .map(OsStr::from_bytes)
                            .map(PathBuf::from)
                    })
                    .expect("failed to parse lsof output: missing cwd")
            })),
        }
    }

    pub fn pid(&self) -> u64 {
        self.pid
    }

    pub fn current_dir(&self) -> &Path {
        self.cwd.as_path()
    }
}

pub fn nvim_sockets() -> Result<impl Iterator<Item = (Process, OsString)>> {
    // ask a temporary, headless nvim, where it puts its sockets
    let run_dir = Command::new("nvim")
        .args([
            "--headless",
            "-c",
            "lua print(vim.fn.stdpath('run'))",
            "-c",
            "q",
        ])
        .output()?
        .stderr;
    let run_dir = Path::new(OsStr::from_bytes(&run_dir));
    // move to the parent, where all "run" directories will be present
    let Some(run_dir_parent) = run_dir.parent() else {
        bail!("Couldn't figure out Neovim's 'run' directory location");
    };

    Ok(WalkDir::new(run_dir_parent)
        .min_depth(2)
        .into_iter()
        .filter_map(|entry| entry.inspect_err(|e| debug!("{e}")).ok())
        .filter_map(|entry| {
            trace!("Checking {}", entry.path().display());

            let name = entry.file_name().to_str()?;
            let mut name_parts = name.split('.');
            let Some("nvim") = name_parts.next() else {
                return None;
            };
            let pid = name_parts.next()?;
            let pid = pid.parse().ok()?;

            entry
                .metadata()
                .inspect_err(|e| debug!("{e}"))
                .ok()?
                .file_type()
                .is_socket()
                .then(|| (Process::from_pid(pid), entry.path().as_os_str().to_owned()))
        }))
}
