proses 0.1.1

Proses – Professional Secure Execution System
use std::collections::HashMap;

use crate::daemon::ipc::{self, Request};

// ── Output helpers ────────────────────────────────────────────────────────────

fn ok(msg: &str) {
    println!("{msg}");
}

fn fail(msg: &str) -> ! {
    eprintln!("{msg}");
    std::process::exit(1);
}

fn send_or_fail(req: &Request) -> crate::daemon::ipc::Response {
    match ipc::send(req) {
        Ok(resp) => resp,
        Err(e) => fail(&e.to_string()),
    }
}

// ── Commands ──────────────────────────────────────────────────────────────────

/// `proses start <command> [--name NAME] [--cwd DIR] [--env K=V]`
///
/// If a process with the same name already exists in the daemon the daemon
/// will stop the old instance and re-launch it with the new parameters,
/// effectively acting as a restart-with-new-config.
pub fn start(
    command: String,
    name: Option<String>,
    cwd: Option<String>,
    env_pairs: Vec<String>,
    max_restarts: u32,
) {
    // Default name: first token of the command string.
    let name = name.unwrap_or_else(|| {
        command
            .split_whitespace()
            .next()
            .unwrap_or("process")
            .to_string()
    });

    // Default cwd: current working directory.
    let cwd = cwd.unwrap_or_else(|| {
        std::env::current_dir()
            .map(|p| p.to_string_lossy().into_owned())
            .unwrap_or_else(|_| "/".to_string())
    });

    // Parse KEY=VALUE pairs.
    let env: HashMap<String, String> = env_pairs
        .into_iter()
        .filter_map(|pair| {
            let (k, v) = pair.split_once('=')?;
            Some((k.to_string(), v.to_string()))
        })
        .collect();

    let resp = send_or_fail(&Request::Start {
        name,
        command,
        cwd,
        env,
        max_restarts,
    });

    if resp.success {
        ok(&resp.message);
    } else {
        fail(&resp.message);
    }
}

/// `proses stop <name|id>`
///
/// Sends `SIGTERM` to the process and marks it `Stopped` so the daemon
/// will not attempt to restart it automatically.
pub fn stop(name_or_id: String) {
    let resp = send_or_fail(&Request::Stop {
        name_or_id: name_or_id.clone(),
    });
    if resp.success {
        ok(&resp.message);
    } else {
        fail(&resp.message);
    }
}

/// `proses restart <name|id>`
///
/// Stops the current instance (SIGTERM) and immediately re-spawns it.
pub fn restart(name_or_id: String) {
    let resp = send_or_fail(&Request::Restart {
        name_or_id: name_or_id.clone(),
    });
    if resp.success {
        ok(&resp.message);
    } else {
        fail(&resp.message);
    }
}

/// `proses delete <name|id>`
///
/// Stops the process (if running) and removes it from the daemon's store.
pub fn delete(name_or_id: String) {
    let resp = send_or_fail(&Request::Delete {
        name_or_id: name_or_id.clone(),
    });
    if resp.success {
        ok(&resp.message);
    } else {
        fail(&resp.message);
    }
}

/// `proses save`
///
/// Flushes the current in-memory process list to `~/.proses/store.json`
/// so it survives a daemon restart.
pub fn save() {
    let resp = send_or_fail(&Request::Save);
    if resp.success {
        ok(&resp.message);
    } else {
        fail(&resp.message);
    }
}

/// `proses resurrect`
///
/// Re-launches all processes that were in the `Running` state the last
/// time `save` was called (or the daemon was stopped cleanly).
pub fn resurrect() {
    let resp = send_or_fail(&Request::Resurrect);
    if resp.success {
        ok(&resp.message);
    } else {
        fail(&resp.message);
    }
}

/// `proses show <name|id>`
///
/// Prints detailed information about a single process.
pub fn show(name_or_id: String) {
    let resp = send_or_fail(&Request::Show {
        name_or_id: name_or_id.clone(),
    });

    if !resp.success {
        fail(&resp.message);
    }

    let Some(p) = resp.process else {
        fail("daemon returned no process data");
    };

    let status_str = format!("{}", p.status);
    let uptime = if p.pid > 0 {
        let secs = (chrono::Utc::now() - p.started_at).num_seconds().max(0) as u64;
        format_uptime(secs)
    } else {
        "".to_string()
    };

    println!();
    println!("  {:<16} {}", "id:", p.id);
    println!("  {:<16} {}", "name:", p.name);
    println!("  {:<16} {status_str}", "status:");
    println!(
        "  {:<16} {}",
        "pid:",
        if p.pid > 0 {
            p.pid.to_string()
        } else {
            "".to_string()
        }
    );
    println!("  {:<16} {}", "restarts:", p.restarts);
    println!("  {:<16} {}", "max restarts:", p.max_restarts);
    println!("  {:<16} {uptime}", "uptime:");
    println!("  {:<16} {}", "command:", p.command);
    println!("  {:<16} {}", "cwd:", p.cwd.display());
    println!("  {:<16} {}", "stdout log:", p.log_out.display());
    println!("  {:<16} {}", "stderr log:", p.log_err.display());
    if !p.env.is_empty() {
        println!("  {:<16}", "env:");
        let mut keys: Vec<&String> = p.env.keys().collect();
        keys.sort();
        for k in keys {
            println!("    {}={}", k, p.env[k]);
        }
    }
    println!();
}

/// `proses logs <name|id> [-n LINES]`
///
/// Dumps the last `lines` lines of the process's stdout log to stdout.
/// For a live follow view, use `proses list` (the TUI dashboard).
pub fn logs(name_or_id: String, lines: usize) {
    let resp = send_or_fail(&Request::Logs { name_or_id, lines });

    if !resp.success {
        fail(&resp.message);
    }

    if let Some(content) = resp.logs {
        print!("{content}");
    }
}

// ── Utilities ─────────────────────────────────────────────────────────────────

fn format_uptime(secs: u64) -> String {
    if secs < 60 {
        format!("{secs}s")
    } else if secs < 3600 {
        format!("{}m {}s", secs / 60, secs % 60)
    } else if secs < 86_400 {
        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
    } else {
        format!("{}d {}h", secs / 86_400, (secs % 86_400) / 3600)
    }
}