//! Background task manager: spawn long-running commands detached, with output
//! captured to files under the session directory.

use std::collections::BTreeMap;
use std::fs::File;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Instant;
use tokio::process::{Child, Command};

pub struct BgTask {
    pub id: u32,
    pub command: String,
    pub child: Child,
    pub pid: Option<u32>,
    pub log_path: PathBuf,
    pub started: Instant,
}

#[derive(Default)]
pub struct BackgroundManager {
    next_id: u32,
    tasks: BTreeMap<u32, BgTask>,
    session_dir: Option<PathBuf>,
}

impl BackgroundManager {
    pub fn new() -> Self {
        BackgroundManager { next_id: 1, tasks: BTreeMap::new(), session_dir: None }
    }

    fn session_dir(&mut self) -> PathBuf {
        if let Some(d) = &self.session_dir {
            return d.clone();
        }
        let dir = crate::config::config_dir()
            .join("bg")
            .join(format!("{}", std::process::id()));
        let _ = std::fs::create_dir_all(&dir);
        self.session_dir = Some(dir.clone());
        dir
    }

    pub fn spawn(&mut self, command: &str, cwd: &std::path::Path) -> Result<String, String> {
        let id = self.next_id;
        self.next_id += 1;
        let log_path = self.session_dir().join(format!("task-{id}.log"));
        let log = File::create(&log_path).map_err(|e| format!("cannot create log file: {e}"))?;
        let log_err = log.try_clone().map_err(|e| format!("log clone failed: {e}"))?;

        let mut cmd = Command::new("bash");
        cmd.arg("-c")
            .arg(command)
            .current_dir(cwd)
            .stdin(Stdio::null())
            .stdout(Stdio::from(log))
            .stderr(Stdio::from(log_err));
        // New process group so we can kill the whole tree later (Unix only).
        #[cfg(unix)]
        unsafe {
            cmd.pre_exec(|| {
                libc::setpgid(0, 0);
                Ok(())
            });
        }
        let child = cmd.spawn().map_err(|e| format!("failed to spawn: {e}"))?;
        let pid = child.id();
        self.tasks.insert(
            id,
            BgTask { id, command: command.to_string(), child, pid, log_path: log_path.clone(), started: Instant::now() },
        );
        Ok(format!(
            "Background task started (id: {id}, pid: {}). Logs: {}. Use manage_background_task with action='status'/'logs' and id={id} to check on it.",
            pid.map(|p| p.to_string()).unwrap_or_else(|| "?".into()),
            log_path.display()
        ))
    }

    pub async fn manage(&mut self, action: &str, id: Option<u32>, lines: usize) -> String {
        match action {
            "list" => {
                if self.tasks.is_empty() {
                    return "No background tasks.".into();
                }
                let mut out = String::from("Background tasks:\n");
                let ids: Vec<u32> = self.tasks.keys().copied().collect();
                for tid in ids {
                    let status = self.status_line(tid).await;
                    out.push_str(&status);
                    out.push('\n');
                }
                out
            }
            "status" => match id {
                Some(tid) if self.tasks.contains_key(&tid) => self.status_line(tid).await,
                Some(tid) => format!("No background task with id {tid}."),
                None => "Missing 'id' for status.".into(),
            },
            "logs" => match id.and_then(|tid| self.tasks.get(&tid)) {
                Some(task) => {
                    let content = std::fs::read_to_string(&task.log_path).unwrap_or_default();
                    let tail: Vec<&str> = content.lines().rev().take(lines).collect();
                    let tail: Vec<&str> = tail.into_iter().rev().collect();
                    if tail.is_empty() {
                        format!("Task {} has produced no output yet.", task.id)
                    } else {
                        format!("Last {} lines of task {} output:\n{}", tail.len(), task.id, tail.join("\n"))
                    }
                }
                None => "No such background task.".into(),
            },
            "kill" => match id {
                Some(tid) => {
                    if let Some(task) = self.tasks.get_mut(&tid) {
                        #[cfg(unix)]
                        if let Some(pid) = task.pid {
                            unsafe {
                                libc::killpg(pid as i32, libc::SIGTERM);
                            }
                        }
                        let _ = task.child.kill().await;
                        format!("Background task {tid} terminated.")
                    } else {
                        format!("No background task with id {tid}.")
                    }
                }
                None => "Missing 'id' for kill.".into(),
            },
            other => format!("Unknown action '{other}'. Use status|logs|kill|list."),
        }
    }

    async fn status_line(&mut self, id: u32) -> String {
        let Some(task) = self.tasks.get_mut(&id) else {
            return format!("No background task with id {id}.");
        };
        let elapsed = task.started.elapsed().as_secs();
        match task.child.try_wait() {
            Ok(Some(status)) => format!(
                "Task {id} [{}]: exited with {} after {}s. Logs: {}",
                short(&task.command),
                status.code().map(|c| c.to_string()).unwrap_or_else(|| "signal".into()),
                elapsed,
                task.log_path.display()
            ),
            Ok(None) => format!("Task {id} [{}]: running ({elapsed}s). Logs: {}", short(&task.command), task.log_path.display()),
            Err(e) => format!("Task {id}: status unknown ({e})."),
        }
    }

    /// Kill everything on session end.
    pub async fn shutdown(&mut self) {
        let ids: Vec<u32> = self.tasks.keys().copied().collect();
        for id in ids {
            let _ = self.manage("kill", Some(id), 0).await;
        }
    }
}

fn short(cmd: &str) -> String {
    let c: String = cmd.chars().take(48).collect();
    if c.len() < cmd.len() {
        format!("{c}…")
    } else {
        c
    }
}

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

    #[tokio::test]
    async fn spawn_status_logs_kill() {
        let mut mgr = BackgroundManager::new();
        let msg = mgr.spawn("for i in 1 2 3; do echo tick $i; sleep 0.1; done; sleep 5", std::path::Path::new("/tmp")).unwrap();
        assert!(msg.contains("id: 1"));
        tokio::time::sleep(std::time::Duration::from_millis(600)).await;
        let status = mgr.manage("status", Some(1), 10).await;
        assert!(status.contains("running"), "{status}");
        let logs = mgr.manage("logs", Some(1), 10).await;
        assert!(logs.contains("tick 3"), "{logs}");
        let killed = mgr.manage("kill", Some(1), 0).await;
        assert!(killed.contains("terminated"));
    }
}
