#![allow(dead_code)]

//! Process service for managing child processes
//!
//! This service provides process lifecycle management for CLI commands,
//! particularly for the `dev` and `run` commands that need to manage
//! multiple node processes.
//!
//! This is a thin wrapper around mecha10-runtime's ProcessManager,
//! delegating core process management to the runtime layer.

use crate::paths;
use anyhow::{Context, Result};
use mecha10_runtime::ProcessManager;
use std::collections::HashMap;
use std::path::Path;
use std::process::{Command, Stdio};

/// Process service for managing child processes
///
/// This is a thin wrapper around the runtime's ProcessManager,
/// adding CLI-specific conveniences and delegating core functionality
/// to the runtime layer.
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::ProcessService;
///
/// # async fn example() -> anyhow::Result<()> {
/// let mut service = ProcessService::new();
///
/// // Spawn a node process
/// service.spawn_node("camera_driver", "target/debug/camera_driver", &[])?;
///
/// // Check status
/// let status = service.get_status();
/// println!("Running processes: {:?}", status);
///
/// // Stop a specific process
/// service.stop("camera_driver")?;
///
/// // Cleanup all processes
/// service.cleanup();
/// # Ok(())
/// # }
/// ```
pub struct ProcessService {
    /// Runtime's process manager (handles core lifecycle)
    manager: ProcessManager,
}

impl ProcessService {
    /// Create a new process service
    pub fn new() -> Self {
        Self {
            manager: ProcessManager::new(),
        }
    }

    /// Extract node name from a full identifier
    ///
    /// Handles both formats:
    /// - Full identifier: `@mecha10/simulation-bridge` -> `simulation-bridge`
    /// - Plain name: `simulation-bridge` -> `simulation-bridge`
    fn extract_node_name(identifier: &str) -> String {
        if identifier.starts_with('@') {
            // Full identifier format: @scope/name
            identifier.split('/').next_back().unwrap_or(identifier).to_string()
        } else {
            identifier.to_string()
        }
    }

    /// Track dependency relationship for a node
    ///
    /// # Arguments
    ///
    /// * `node` - Name of the node
    /// * `dependencies` - List of nodes this node depends on
    pub fn track_dependency(&mut self, node: &str, dependencies: Vec<String>) {
        for dep in dependencies {
            self.manager.add_dependency(node.to_string(), dep);
        }
    }

    /// Get shutdown order (reverse dependency order)
    ///
    /// Returns nodes in order they should be stopped:
    /// - Nodes with dependents first (high-level nodes)
    /// - Then their dependencies (low-level nodes)
    ///
    /// This ensures we don't stop a node while other nodes depend on it.
    ///
    /// Delegates to the runtime's ProcessManager.
    pub fn get_shutdown_order(&self) -> Vec<String> {
        self.manager.shutdown_order()
    }

    /// Check if we're in framework development mode
    ///
    /// Framework dev mode is detected by:
    /// 1. MECHA10_FRAMEWORK_PATH environment variable
    /// 2. Existence of .cargo/config.toml with patches
    pub fn is_framework_dev_mode() -> bool {
        // Check MECHA10_FRAMEWORK_PATH
        if std::env::var("MECHA10_FRAMEWORK_PATH").is_ok() {
            return true;
        }

        // Check if .cargo/config.toml exists (indicates framework dev)
        std::path::Path::new(paths::rust::CARGO_CONFIG).exists()
    }

    /// Find globally installed binary for a node
    ///
    /// Searches in:
    /// 1. ~/.cargo/bin/{node_name}
    /// 2. ~/.mecha10/bin/{node_name}
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node (e.g., "simulation-bridge")
    ///
    /// # Returns
    ///
    /// Path to the binary if found, None otherwise
    pub fn find_global_binary(node_name: &str) -> Option<std::path::PathBuf> {
        // Try ~/.cargo/bin/ first
        let cargo_bin = paths::user::cargo_bin(node_name);
        if cargo_bin.exists() && cargo_bin.is_file() {
            return Some(cargo_bin);
        }

        // Try ~/.mecha10/bin/
        let mecha10_bin = paths::user::bin(node_name);
        if mecha10_bin.exists() && mecha10_bin.is_file() {
            return Some(mecha10_bin);
        }

        None
    }

    /// Resolve binary path for a node with smart resolution
    ///
    /// Resolution strategy:
    /// 1. If framework dev mode: use local build (target/debug or target/release)
    /// 2. If global binary exists: use global binary
    /// 3. Fallback: use local build path
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node
    /// * `is_monorepo_node` - Whether this is a framework node
    /// * `project_name` - Name of the project
    ///
    /// # Returns
    ///
    /// Path to the binary to execute
    pub fn resolve_node_binary(node_name: &str, is_monorepo_node: bool, project_name: &str) -> String {
        // Framework dev mode: always use local builds
        if Self::is_framework_dev_mode() {
            return Self::get_local_binary_path(node_name, is_monorepo_node, project_name);
        }

        // For monorepo (framework) nodes, check for global installation
        if is_monorepo_node {
            if let Some(global_path) = Self::find_global_binary(node_name) {
                return global_path.to_string_lossy().to_string();
            }
        }

        // Fallback to local build
        Self::get_local_binary_path(node_name, is_monorepo_node, project_name)
    }

    /// Get local binary path (in target/ directory)
    fn get_local_binary_path(node_name: &str, is_monorepo_node: bool, project_name: &str) -> String {
        if is_monorepo_node {
            // Monorepo nodes run via the project binary with 'node' subcommand
            paths::target_path("release", project_name)
        } else {
            // Local nodes have their own binary
            paths::target_path("release", node_name)
        }
    }

    /// Resolve path to mecha10-node-runner binary
    ///
    /// Resolution strategy:
    /// 1. Framework dev mode: $MECHA10_FRAMEWORK_PATH/target/release/mecha10-node-runner
    /// 2. Global installation: ~/.cargo/bin/mecha10-node-runner
    /// 3. Fallback: "mecha10-node-runner" (rely on PATH)
    ///
    /// # Returns
    ///
    /// Path to the mecha10-node-runner binary
    pub fn resolve_node_runner_path() -> String {
        // Framework dev mode: use framework's target directory
        if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
            let framework_binary =
                std::path::PathBuf::from(&framework_path).join(paths::framework::NODE_RUNNER_RELEASE);

            if framework_binary.exists() {
                return framework_binary.to_string_lossy().to_string();
            }

            // Try debug build if release not available
            let framework_binary_debug =
                std::path::PathBuf::from(&framework_path).join(paths::framework::NODE_RUNNER_DEBUG);

            if framework_binary_debug.exists() {
                return framework_binary_debug.to_string_lossy().to_string();
            }
        }

        // Try global installation
        if let Some(global_path) = Self::find_global_binary("mecha10-node-runner") {
            return global_path.to_string_lossy().to_string();
        }

        // Fallback: rely on PATH
        "mecha10-node-runner".to_string()
    }

    /// Spawn a node process
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    ///
    /// # Errors
    ///
    /// Returns an error if the process cannot be spawned
    pub fn spawn_node(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
        let child = Command::new(binary_path)
            .args(args)
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn a process with output capture
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    ///
    /// # Errors
    ///
    /// Returns an error if the process cannot be spawned
    pub fn spawn_with_output(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
        let child = Command::new(binary_path)
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn a process with custom environment variables
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    /// * `env` - Environment variables to set
    pub fn spawn_with_env(
        &mut self,
        name: &str,
        binary_path: &str,
        args: &[&str],
        env: HashMap<String, String>,
    ) -> Result<u32> {
        // Create logs directory if it doesn't exist
        let logs_dir = std::path::PathBuf::from(paths::project::LOGS_DIR);
        if !logs_dir.exists() {
            std::fs::create_dir_all(&logs_dir)?;
        }

        // Create log file for this process
        // Use short node name for log file (e.g., "@mecha10/listener" -> "listener.log")
        let log_name = Self::extract_node_name(name);
        let log_file_path = logs_dir.join(format!("{}.log", log_name));
        let log_file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&log_file_path)
            .with_context(|| format!("Failed to create log file: {}", log_file_path.display()))?;

        // Clone for stderr
        let log_file_stderr = log_file.try_clone().context("Failed to clone log file handle")?;

        // Debug: Log environment being passed to process
        tracing::debug!("🔧 spawn_with_env for '{}': received {} env vars", name, env.len());
        if !env.is_empty() {
            for (key, value) in &env {
                if key == "ROBOT_API_KEY" {
                    tracing::debug!("  {} = <redacted>", key);
                } else {
                    tracing::debug!("  {} = {}", key, value);
                }
            }
        } else {
            tracing::warn!("⚠️  No environment variables to inject!");
        }

        let mut cmd = Command::new(binary_path);
        cmd.args(args)
            .envs(&env)
            .stdout(log_file) // Redirect stdout to log file
            .stderr(log_file_stderr); // Redirect stderr to log file

        // On Unix: Create new process group to prevent terminal signals from reaching child processes
        // This ensures Ctrl+C in the terminal only affects the main CLI process, not node-runner
        #[cfg(unix)]
        {
            use std::os::unix::process::CommandExt;
            cmd.process_group(0); // 0 = create new process group with same ID as child PID
        }

        let child = cmd
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn a process in a specific working directory
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    /// * `working_dir` - Working directory for the process
    pub fn spawn_in_dir(
        &mut self,
        name: &str,
        binary_path: &str,
        args: &[&str],
        working_dir: impl AsRef<Path>,
    ) -> Result<u32> {
        let child = Command::new(binary_path)
            .args(args)
            .current_dir(working_dir)
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn multiple node processes from a list
    ///
    /// # Arguments
    ///
    /// * `nodes` - Vec of (name, binary_path, args) tuples
    ///
    /// # Returns
    ///
    /// HashMap of node names to PIDs
    pub fn spawn_nodes(&mut self, nodes: Vec<(&str, &str, Vec<&str>)>) -> Result<HashMap<String, u32>> {
        let mut pids = HashMap::new();

        for (name, binary_path, args) in nodes {
            match self.spawn_node(name, binary_path, &args) {
                Ok(pid) => {
                    pids.insert(name.to_string(), pid);
                }
                Err(e) => {
                    eprintln!("Failed to spawn {}: {}", name, e);
                }
            }
        }

        Ok(pids)
    }

    /// Get status of all processes
    ///
    /// Returns a HashMap mapping process names to status strings
    pub fn get_status(&mut self) -> HashMap<String, String> {
        use mecha10_runtime::ProcessStatus;

        self.manager
            .status_all()
            .into_iter()
            .map(|(name, status)| {
                let status_str = match status {
                    ProcessStatus::Running => "running".to_string(),
                    ProcessStatus::Exited(code) => format!("exited (code: {})", code),
                    ProcessStatus::Error => "error".to_string(),
                };
                (name, status_str)
            })
            .collect()
    }

    /// Get the number of tracked processes
    pub fn count(&self) -> usize {
        self.manager.len()
    }

    /// Check if any processes are being tracked
    pub fn is_empty(&self) -> bool {
        self.manager.is_empty()
    }

    /// Stop a specific process by name
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the process to stop
    ///
    /// # Errors
    ///
    /// Returns an error if the process is not found
    ///
    /// Note: This uses a default 10-second timeout for graceful shutdown.
    /// Use stop_with_timeout() for custom timeout.
    pub fn stop(&mut self, name: &str) -> Result<()> {
        self.manager.stop_graceful(name, std::time::Duration::from_secs(10))
    }

    /// Stop a process with timeout for graceful shutdown
    ///
    /// Tries graceful shutdown (SIGTERM on Unix), then force kills after timeout
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the process to stop
    /// * `timeout` - How long to wait for graceful shutdown
    ///
    /// # Errors
    ///
    /// Returns an error if process not found or cannot be stopped
    pub fn stop_with_timeout(&mut self, name: &str, timeout: std::time::Duration) -> Result<()> {
        self.manager.stop_graceful(name, timeout)
    }

    /// Force kill a process by name
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the process to kill
    pub fn force_kill(&mut self, name: &str) -> Result<()> {
        self.manager.force_kill(name)
    }

    /// Stop all processes gracefully in dependency order
    ///
    /// This delegates to the runtime's ProcessManager which handles:
    /// - Dependency-based shutdown ordering
    /// - Graceful shutdown with timeout
    /// - Force kill fallback
    pub fn cleanup(&mut self) {
        self.manager.shutdown_all();
    }

    /// Check if a process is running
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the process to check
    pub fn is_running(&mut self, name: &str) -> bool {
        self.manager.is_running(name)
    }

    /// Get access to the underlying ProcessManager
    ///
    /// Useful for advanced operations or when migrating existing code.
    /// Provides direct access to the runtime's ProcessManager.
    pub fn manager(&mut self) -> &mut ProcessManager {
        &mut self.manager
    }

    /// Build a node binary if needed
    ///
    /// Helper method to build a specific node package
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node to build
    /// * `release` - Whether to build in release mode
    pub fn build_node(&self, node_name: &str, release: bool) -> Result<()> {
        let mut cmd = Command::new("cargo");
        cmd.arg("build");

        if release {
            cmd.arg("--release");
        }

        cmd.arg("--bin").arg(node_name);

        let output = cmd
            .output()
            .with_context(|| format!("Failed to build node: {}", node_name))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(anyhow::anyhow!("Build failed for {}: {}", node_name, stderr));
        }

        Ok(())
    }

    /// Build all nodes in the workspace
    ///
    /// # Arguments
    ///
    /// * `release` - Whether to build in release mode
    pub fn build_all(&self, release: bool) -> Result<()> {
        let mut cmd = Command::new("cargo");
        cmd.arg("build");

        if release {
            cmd.arg("--release");
        }

        cmd.arg("--all");

        let output = cmd.output().context("Failed to build workspace")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(anyhow::anyhow!("Build failed: {}", stderr));
        }

        Ok(())
    }

    /// Build a binary from the framework monorepo
    ///
    /// This builds a binary from the framework path (MECHA10_FRAMEWORK_PATH).
    /// Used for binaries like `mecha10-node-runner` that exist in the monorepo
    /// but need to be built when running from a generated project.
    ///
    /// # Arguments
    ///
    /// * `package_name` - Name of the package to build (e.g., "mecha10-node-runner")
    /// * `release` - Whether to build in release mode
    ///
    /// # Returns
    ///
    /// Ok(()) on success, or error if build fails or framework path not set
    pub fn build_from_framework(&self, package_name: &str, release: bool) -> Result<()> {
        // Get framework path from environment
        let framework_path = std::env::var("MECHA10_FRAMEWORK_PATH")
            .context("MECHA10_FRAMEWORK_PATH not set - cannot build from framework")?;

        let mut cmd = Command::new("cargo");
        cmd.arg("build");

        if release {
            cmd.arg("--release");
        }

        cmd.arg("-p").arg(package_name);
        cmd.current_dir(&framework_path);

        let output = cmd
            .output()
            .with_context(|| format!("Failed to build package from framework: {}", package_name))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(anyhow::anyhow!(
                "Build failed for {} (from framework): {}",
                package_name,
                stderr
            ));
        }

        Ok(())
    }

    /// Build only packages needed by the current project (smart selective build)
    ///
    /// For generated projects, this just builds the project binary.
    /// Cargo automatically builds only the dependencies actually used.
    /// With .cargo/config.toml patches, this rebuilds framework packages from source.
    ///
    /// # Arguments
    ///
    /// * `release` - Whether to build in release mode
    ///
    /// # Returns
    ///
    /// Ok(()) on success, or error if build fails
    pub fn build_project_packages(&self, release: bool) -> Result<()> {
        use crate::types::ProjectConfig;

        // Load project config
        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
        if !config_path.exists() {
            // Fallback to build_all if no project config
            return self.build_all(release);
        }

        // Parse config to get project name
        let config_content = std::fs::read_to_string(config_path)?;
        let config: ProjectConfig = serde_json::from_str(&config_content)?;

        // Build just the project binary
        // Cargo will automatically:
        // 1. Resolve dependencies from Cargo.toml
        // 2. Apply .cargo/config.toml patches (framework dev mode)
        // 3. Build only the dependencies actually used
        // 4. Use incremental compilation for unchanged code
        let mut cmd = Command::new("cargo");
        cmd.arg("build");

        if release {
            cmd.arg("--release");
        }

        // Build the project binary - Cargo handles the rest
        cmd.arg("--bin").arg(&config.name);

        let output = cmd.output().context("Failed to build project")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(anyhow::anyhow!("Build failed: {}", stderr));
        }

        Ok(())
    }

    /// Restart a specific process
    ///
    /// Stops the process if running and starts it again
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the process
    /// * `binary_path` - Path to the binary
    /// * `args` - Command-line arguments
    pub fn restart(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
        // Stop if running
        if self.is_running(name) {
            self.stop(name)?;
            // Give it a moment to shutdown
            std::thread::sleep(std::time::Duration::from_millis(100));
        }

        // Start again
        self.spawn_node(name, binary_path, args)
    }

    /// Restart all processes
    ///
    /// # Arguments
    ///
    /// * `nodes` - Vec of (name, binary_path, args) tuples
    pub fn restart_all(&mut self, nodes: Vec<(&str, &str, Vec<&str>)>) -> Result<HashMap<String, u32>> {
        // Stop all
        self.cleanup();

        // Small delay for cleanup
        std::thread::sleep(std::time::Duration::from_millis(500));

        // Start all
        self.spawn_nodes(nodes)
    }

    /// Spawn a node using mecha10-node-runner
    ///
    /// This is the simplified spawning method for Phase 2+ of Node Lifecycle Architecture.
    /// It delegates all complexity (binary resolution, model pulling, env setup) to node-runner.
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node to run
    ///
    /// # Returns
    ///
    /// Process ID of the spawned node-runner instance
    ///
    /// # Errors
    ///
    /// Returns an error if the node-runner cannot be spawned
    ///
    /// # Configuration
    ///
    /// The node-runner reads configuration from the node's config file (e.g., `configs/nodes/{node_name}.json`)
    /// and supports the following runtime settings:
    ///
    /// ```json
    /// {
    ///   "runtime": {
    ///     "restart_policy": "on-failure",  // never, on-failure, always
    ///     "max_retries": 3,
    ///     "backoff_secs": 1
    ///   },
    ///   "depends_on": ["camera", "lidar"],
    ///   "startup_timeout_secs": 30
    /// }
    /// ```
    ///
    /// To enable dependency checking, use: `mecha10-node-runner --wait-for-deps <node-name>`
    pub fn spawn_node_runner(
        &mut self,
        node_identifier: &str,
        project_env: Option<HashMap<String, String>>,
    ) -> Result<u32> {
        // Extract node name from full identifier (e.g., @mecha10/listener -> listener)
        let node_name = Self::extract_node_name(node_identifier);

        // Simply spawn: mecha10-node-runner <node-name>
        // The node-runner handles:
        // - Binary path resolution (monorepo vs local)
        // - Model pulling (if node needs AI models)
        // - Environment setup
        // - Log redirection
        // - Restart policies (from config)
        // - Health monitoring
        // - Dependency checking (if --wait-for-deps enabled)
        // - Actual node execution

        // Resolve path to mecha10-node-runner binary
        let runner_path = Self::resolve_node_runner_path();

        // Build environment - include project vars for config substitution
        let env = project_env.unwrap_or_default();

        // Register with full identifier (e.g., @mecha10/listener) for consistent tracking
        // The node-runner only needs the short name as an argument
        self.spawn_with_env(node_identifier, &runner_path, &[&node_name], env)
    }

    /// Spawn a node directly via the project binary (standalone mode)
    ///
    /// This is used when mecha10-node-runner is not available (standalone projects
    /// installed from crates.io). Handles both bundled nodes (via CLI) and local
    /// project nodes (built and run directly).
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node to run
    /// * `project_name` - Name of the project (used to find the binary)
    /// * `project_env` - Optional additional environment variables from project config
    ///
    /// # Returns
    ///
    /// Process ID of the spawned node process
    pub fn spawn_node_direct(
        &mut self,
        node_identifier: &str,
        _project_name: &str,
        project_env: Option<HashMap<String, String>>,
    ) -> Result<u32> {
        // Extract node name from full identifier (e.g., @mecha10/listener -> listener)
        let node_name = Self::extract_node_name(node_identifier);

        // Check if this is a local project node by looking at mecha10.json
        let is_local_node = self.is_local_project_node(&node_name);

        let mut env = HashMap::new();
        // Use full identifier for NODE_NAME so health reports and dashboard are consistent
        env.insert("NODE_NAME".to_string(), node_identifier.to_string());
        if let Ok(rust_log) = std::env::var("RUST_LOG") {
            env.insert("RUST_LOG".to_string(), rust_log);
        }

        // Add project environment variables (REDIS_URL, control plane, robot info, etc.)
        if let Some(project_vars) = project_env {
            tracing::debug!(
                "🔀 spawn_node_direct for '{}': merging {} project vars",
                node_name,
                project_vars.len()
            );
            for (key, value) in &project_vars {
                tracing::debug!(
                    "  Adding: {} = {}",
                    key,
                    if key == "ROBOT_API_KEY" { "<redacted>" } else { value }
                );
            }
            env.extend(project_vars);
        } else {
            tracing::warn!("⚠️  spawn_node_direct for '{}': No project_env provided!", node_name);
        }

        tracing::debug!(
            "🏁 spawn_node_direct for '{}': final env has {} vars, is_local: {}",
            node_name,
            env.len(),
            is_local_node
        );

        if is_local_node {
            // Local project node: build and run the binary directly
            // Register with full identifier (e.g., @local/my-node) for consistent tracking
            self.spawn_local_node_with_identifier(node_identifier, &node_name, env)
        } else {
            // Bundled node: use CLI's bundled nodes via `mecha10 node <name>`
            // Register with full identifier (e.g., @mecha10/speaker) for consistent tracking
            let binary_path = Self::resolve_cli_binary()?;
            self.spawn_with_env(node_identifier, &binary_path, &["node", &node_name], env)
        }
    }

    /// Check if a node is a local project node (defined in nodes/ directory)
    fn is_local_project_node(&self, node_name: &str) -> bool {
        use crate::types::{NodeSource, ProjectConfig};

        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
        if !config_path.exists() {
            return false;
        }

        let config_content = match std::fs::read_to_string(config_path) {
            Ok(c) => c,
            Err(_) => return false,
        };

        let config: ProjectConfig = match serde_json::from_str(&config_content) {
            Ok(c) => c,
            Err(_) => return false,
        };

        // Check if node exists as a project node (nodes/<name>)
        config
            .nodes
            .find_by_name(node_name)
            .map(|spec| spec.source == NodeSource::Project)
            .unwrap_or(false)
    }

    /// Spawn a local project node with full identifier for tracking
    ///
    /// Builds the node with cargo and runs the binary directly.
    /// Registers the process with the full identifier (e.g., @local/my-node) for consistent tracking.
    fn spawn_local_node_with_identifier(
        &mut self,
        node_identifier: &str,
        node_name: &str,
        env: HashMap<String, String>,
    ) -> Result<u32> {
        use std::process::Command;

        // First, build the node
        tracing::info!("🔨 Building local node: {}", node_name);

        let build_output = Command::new("cargo")
            .args(["build", "-p", node_name])
            .output()
            .context("Failed to run cargo build")?;

        if !build_output.status.success() {
            let stderr = String::from_utf8_lossy(&build_output.stderr);
            anyhow::bail!("Failed to build node '{}': {}", node_name, stderr);
        }

        tracing::info!("✅ Built local node: {}", node_name);

        // Run the binary from target/debug/<name>
        let binary_path = paths::target_path("debug", node_name);

        if !std::path::Path::new(&binary_path).exists() {
            anyhow::bail!(
                "Binary not found at '{}'. Build may have failed or the package name differs from node name.",
                binary_path
            );
        }

        // Register with full identifier for consistent tracking
        self.spawn_with_env(node_identifier, &binary_path, &[], env)
    }

    /// Resolve path to mecha10 CLI binary
    fn resolve_cli_binary() -> Result<String> {
        // First try to find mecha10 in PATH
        if let Some(path) = Self::find_global_binary("mecha10") {
            return Ok(path.to_string_lossy().to_string());
        }

        // Try common install locations
        let locations = [paths::user::local_bin("mecha10"), paths::user::cargo_bin("mecha10")];

        for path in &locations {
            if path.exists() {
                return Ok(path.to_string_lossy().to_string());
            }
        }

        // Fall back to current executable (we are mecha10!)
        if let Ok(exe) = std::env::current_exe() {
            return Ok(exe.to_string_lossy().to_string());
        }

        anyhow::bail!("Could not find mecha10 CLI binary. Ensure it's installed and in your PATH.")
    }

    /// Check if mecha10-node-runner is available
    pub fn is_node_runner_available() -> bool {
        // Check framework path first
        if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
            let release = std::path::PathBuf::from(&framework_path).join(paths::framework::NODE_RUNNER_RELEASE);
            let debug = std::path::PathBuf::from(&framework_path).join(paths::framework::NODE_RUNNER_DEBUG);
            if release.exists() || debug.exists() {
                return true;
            }
        }

        // Check global installation
        Self::find_global_binary("mecha10-node-runner").is_some()
    }
}

impl Default for ProcessService {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for ProcessService {
    fn drop(&mut self) {
        // ProcessManager's Drop will handle graceful shutdown
    }
}
