//! The editor's command terminal: runs a shell command and streams its output
//! into the [Console](crate::console) panel.
//!
//! This is the second half of the Stage-10 editor terminal — the log-capture
//! Console shows engine/script output, and this adds **command execution**: type
//! a command, it runs (via `sh -c`) with the working directory set to the open
//! project, and its stdout/stderr stream back into the same panel as they
//! arrive. Long-running commands (a build, a watcher, an AI-agent CLI) stream
//! line by line rather than blocking the editor — each line is pushed to the
//! shared console buffer from a reader thread, and the panel re-renders it next
//! frame.
//!
//! Running arbitrary commands from the editor is intended: the terminal is the
//! drop-in surface for dev tools and AI agents that edit the watched scripts
//! (whose edits then flow back through live reload).

use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use std::process::{Command, Stdio};

use log::Level;

use crate::console;

/// The console target terminal lines are tagged with (distinguishes shell output
/// from engine `log` records in the panel).
const TARGET: &str = "terminal";

/// Spawns `command` with `sh -c` in `cwd`, streaming its stdout/stderr into the
/// console. Returns immediately; output arrives asynchronously. A blank command
/// is ignored.
///
/// The command is echoed first (`$ <command>`); stdout lines log at info level,
/// stderr at warn (so errors stand out), and the exit status is reported when
/// the process finishes.
pub fn run(command: &str, cwd: &Path) {
    let command = command.trim();
    if command.is_empty() {
        return;
    }
    console::append(Level::Info, TARGET, format!("$ {command}"));

    let child = Command::new("sh")
        .arg("-c")
        .arg(command)
        .current_dir(cwd)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn();

    let mut child = match child {
        Ok(child) => child,
        Err(err) => {
            console::append(Level::Error, TARGET, format!("failed to start: {err}"));
            return;
        }
    };

    let stdout = child.stdout.take();
    let stderr = child.stderr.take();

    // One supervisor thread owns the child: it streams both pipes (stderr on its
    // own thread so the two don't deadlock on full buffers), waits, and reports
    // the exit status. Detached — the panel reads results from the shared buffer.
    std::thread::spawn(move || {
        let err_thread = stderr.map(|e| std::thread::spawn(move || stream(e, Level::Warn)));
        if let Some(out) = stdout {
            stream(out, Level::Info);
        }
        if let Some(handle) = err_thread {
            let _ = handle.join();
        }
        match child.wait() {
            Ok(status) if status.success() => {
                console::append(Level::Info, TARGET, "(exit 0)");
            }
            Ok(status) => {
                let code = status
                    .code()
                    .map(|c| c.to_string())
                    .unwrap_or_else(|| "signal".to_string());
                console::append(Level::Warn, TARGET, format!("(exit {code})"));
            }
            Err(err) => console::append(Level::Error, TARGET, format!("wait failed: {err}")),
        }
    });
}

/// Reads `reader` line by line, pushing each line into the console at `level`.
fn stream<R: Read>(reader: R, level: Level) {
    let mut buf = BufReader::new(reader);
    let mut line = String::new();
    loop {
        line.clear();
        match buf.read_line(&mut line) {
            Ok(0) => break, // EOF
            Ok(_) => console::append(
                level,
                TARGET,
                line.trim_end_matches(['\n', '\r']).to_string(),
            ),
            Err(_) => break,
        }
    }
}
