moeix 0.14.0

Sub-millisecond code search via sparse trigram indexing.
use std::path::Path;

use crate::args::{BeaconStatusJson, ServiceAction, SimpleStatusJson};
use crate::commands::find_index;
use crate::output::format_uptime;

#[cfg(all(feature = "notify", target_os = "linux"))]
pub(crate) fn find_systemctl() -> std::ffi::OsString {
    let usr_bin = std::path::Path::new("/usr/bin/systemctl");
    if usr_bin.exists() {
        return usr_bin.as_os_str().to_os_string();
    }
    let bin = std::path::Path::new("/bin/systemctl");
    if bin.exists() {
        return bin.as_os_str().to_os_string();
    }
    // Fallback to path lookup if neither exists
    std::ffi::OsString::from("systemctl")
}

#[cfg(all(feature = "notify", not(target_os = "linux")))]
#[allow(dead_code)]
pub(crate) fn find_systemctl() -> std::ffi::OsString {
    std::ffi::OsString::from("systemctl")
}

fn find_ixd() -> std::path::PathBuf {
    if let Ok(exe) = std::env::current_exe()
        && let Some(parent) = exe.parent()
    {
        let candidate = parent.join("ixd");
        if candidate.exists() {
            return candidate;
        }
    }
    std::path::PathBuf::from("ixd")
}

#[cfg(feature = "notify")]
pub(crate) fn handle_service(action: &ServiceAction) -> ix::error::Result<()> {
    match action {
        ServiceAction::Status { path, json } => {
            handle_service_status(path.as_deref(), *json);
            Ok(())
        }
        ServiceAction::Start { path } => {
            let ixd = find_ixd();
            let watch_path = path
                .clone()
                .unwrap_or_else(|| std::path::PathBuf::from("."));
            let status = std::process::Command::new(&ixd)
                .arg("--daemon")
                .arg(&watch_path)
                .status()
                .map_err(|e| {
                    ix::error::Error::Config(format!("failed to execute {}: {e}", ixd.display()))
                })?;
            if !status.success() {
                return Err(ix::error::Error::Config(
                    "failed to start ixd daemon".into(),
                ));
            }
            println!("ixd daemon started for {}.", watch_path.display());
            println!("Run 'ix service status' to verify.");
            Ok(())
        }
        ServiceAction::Stop { path } => {
            let ixd = find_ixd();
            let stop_path = path
                .clone()
                .unwrap_or_else(|| std::path::PathBuf::from("."));
            let status = std::process::Command::new(&ixd)
                .arg("--stop")
                .arg(&stop_path)
                .status()
                .map_err(|e| {
                    ix::error::Error::Config(format!("failed to execute {}: {e}", ixd.display()))
                })?;
            if !status.success() {
                return Err(ix::error::Error::Config("failed to stop ixd daemon".into()));
            }
            Ok(())
        }
        ServiceAction::Restart { path } => {
            let ixd = find_ixd();
            let target_path = path
                .clone()
                .unwrap_or_else(|| std::path::PathBuf::from("."));
            let _ = std::process::Command::new(&ixd)
                .arg("--stop")
                .arg(&target_path)
                .status();
            std::thread::sleep(std::time::Duration::from_millis(500));
            let status = std::process::Command::new(&ixd)
                .arg("--daemon")
                .arg(&target_path)
                .status()
                .map_err(|e| {
                    ix::error::Error::Config(format!("failed to execute {}: {e}", ixd.display()))
                })?;
            if !status.success() {
                return Err(ix::error::Error::Config(
                    "failed to restart ixd daemon".into(),
                ));
            }
            println!("ixd daemon restarted for {}.", target_path.display());
            Ok(())
        }
        ServiceAction::Install { path } => {
            #[cfg(target_os = "linux")]
            {
                let home = std::env::var("HOME")
                    .map_err(|_| ix::error::Error::Config("HOME not set".into()))?;
                let service_dir = std::path::PathBuf::from(&home).join(".config/systemd/user");
                let service_file = service_dir.join("ixd.service");

                let watch_path = path.clone().unwrap_or_else(|| {
                    std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(&home))
                });
                let watch_path_abs = watch_path.canonicalize().unwrap_or(watch_path);

                let ixd_path = find_ixd();
                let ixd_abs = ixd_path.canonicalize().unwrap_or(ixd_path);

                std::fs::create_dir_all(&service_dir)?;

                let service_content = format!(
                    r"[Unit]
Description=ix background daemon
After=network.target

[Service]
ExecStart={} {}
Restart=on-failure
RestartSec=10
StartLimitBurst=3
StartLimitIntervalSec=60

[Install]
WantedBy=default.target
",
                    ixd_abs.display(),
                    watch_path_abs.display()
                );

                std::fs::write(&service_file, service_content)?;

                let status = std::process::Command::new(find_systemctl())
                    .args(["--user", "daemon-reload"])
                    .status();
                match status {
                    Ok(s) if s.success() => {
                        println!("ixd service installed at {}", service_file.display());
                        println!("Watch path: {}", watch_path_abs.display());
                        println!("Run 'ix service start' to start the daemon.");
                    }
                    _ => {
                        println!("ixd service file written to {}", service_file.display());
                        println!(
                            "Note: systemd daemon-reload was not run or systemd is not active on this system."
                        );
                        println!("You can run 'ix service start' directly to run ixd natively.");
                    }
                }
                Ok(())
            }
            #[cfg(not(target_os = "linux"))]
            {
                let _ = path;
                println!("Systemd installation is only applicable to Linux.");
                println!(
                    "On this platform, run 'ix service start' directly to launch ixd in background."
                );
                Ok(())
            }
        }
    }
}

#[cfg(not(feature = "notify"))]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn handle_service(action: &ServiceAction) -> ix::error::Result<()> {
    if let ServiceAction::Status { path, json } = &action {
        handle_service_status(path.as_deref(), *json);
        return Ok(());
    }
    eprintln!("Error: ix service commands require the 'notify' feature.");
    eprintln!("Install with: cargo install moeix --features notify");
    std::process::exit(1);
}

pub(crate) fn handle_service_status(path: Option<&Path>, json: bool) {
    use std::time::{SystemTime, UNIX_EPOCH};

    let search_path = path.unwrap_or(Path::new("."));
    let beacon_opt = find_index(search_path).and_then(|(_, _, beacon)| beacon);

    match beacon_opt {
        Some(beacon) if beacon.is_live() => {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            let uptime = now.saturating_sub(beacon.start_time);
            if json {
                print_json_status("running", &beacon, Some(uptime));
            } else {
                println!("ixd daemon is running");
                println!("  PID: {}", beacon.pid);
                println!("  Uptime: {}", format_uptime(uptime));
                println!("  Status: {}", beacon.status);
                println!("  Root: {}", beacon.root.display());
                if let Some(ref sock) = beacon.socket_path {
                    println!("  Socket: {}", sock.display());
                }
            }
        }
        Some(beacon) => {
            #[cfg(unix)]
            let is_orphan = {
                use nix::sys::signal::kill;
                use nix::unistd::Pid;
                kill(Pid::from_raw(beacon.pid), None).is_ok()
            };
            #[cfg(not(unix))]
            let is_orphan = false;

            if is_orphan {
                if json {
                    let out = SimpleStatusJson {
                        status: "orphan".to_string(),
                        stale_pid: Some(beacon.pid),
                    };
                    if let Ok(s) = serde_json::to_string(&out) {
                        println!("{s}");
                    }
                } else {
                    println!("PID {} is not ixd (orphan beacon)", beacon.pid);
                }
            } else if json {
                let out = SimpleStatusJson {
                    status: "dead".to_string(),
                    stale_pid: Some(beacon.pid),
                };
                if let Ok(s) = serde_json::to_string(&out) {
                    println!("{s}");
                }
            } else {
                println!(
                    "ixd daemon is not running (stale beacon from PID {})",
                    beacon.pid
                );
            }
        }
        None => {
            if json {
                let out = SimpleStatusJson {
                    status: "not_running".to_string(),
                    stale_pid: None,
                };
                if let Ok(s) = serde_json::to_string(&out) {
                    println!("{s}");
                }
            } else {
                println!("ixd daemon is not running");
            }
        }
    }
}

pub(crate) fn print_json_status(status: &str, beacon: &ix::format::Beacon, uptime: Option<u64>) {
    let json = BeaconStatusJson {
        status: status.to_string(),
        pid: beacon.pid,
        uptime_secs: uptime,
        daemon_status: beacon.status.clone(),
        root: beacon.root.display().to_string(),
        socket: beacon.socket_path.as_ref().map(|p| p.display().to_string()),
        instance_id: beacon.instance_id,
    };
    // Use compact serde_json output to match the original hand-built JSON format.
    // Errors on serialization are extremely unlikely (all fields are simple types)
    // but we fall back to a minimal string to avoid silent output loss.
    match serde_json::to_string(&json) {
        Ok(s) => println!("{s}"),
        Err(e) => eprintln!("ix: warning: JSON serialize error: {e}"),
    }
}