/// ACP Runtime Manager — manages agent runtime subprocess lifecycle.
///
/// Supports multiple runtimes (Hermes, Reasonix) and switching between them.
/// Each runtime is spawned as a subprocess communicating via the ACP JSON-RPC 2.0 protocol.
///
/// Configuration:
///   `MNOTE_WEB_ACP_DEFAULT_RUNTIME` — default runtime name ("hermes" | "reasonix")
///   `MNOTE_WEB_HERMES_BIN` — path to Hermes binary (default: "hermes")
///   `MNOTE_WEB_HERMES_ACP_PROFILE` — Hermes profile for ACP runtime (default: "default")
///   `MNOTE_WEB_REASONIX_WRAPPER` — path to Reasonix wrapper script (default: "scripts/reasonix-acp-wrapper.mjs")
///
/// Or via JSON env var:
///   `MNOTE_WEB_ACP_RUNTIMES` — JSON array of runtime configs
use crate::acp_client::{AcpClient, AcpError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{timeout, Duration};
use tracing::{debug, info, warn};

// ── Config ───────────────────────────────────────────

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcpRuntimeConfig {
    /// Display name (e.g. "hermes", "reasonix").
    pub name: String,
    /// Binary path (e.g. "hermes", "node").
    pub bin: String,
    /// Command arguments (e.g. ["acp"], ["scripts/reasonix-acp-wrapper.mjs"]).
    #[serde(default)]
    pub args: Vec<String>,
    /// Extra environment variables.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub env: Option<HashMap<String, String>>,
    /// Human-readable title for the runtime selector.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
}

impl AcpRuntimeConfig {
    /// Create a Hermes ACP runtime config.
    pub fn hermes(bin: Option<&str>, profile: Option<&str>) -> Self {
        let profile = profile.unwrap_or("default").trim();
        let args = if profile.is_empty() {
            vec!["acp".into()]
        } else {
            vec!["-p".into(), profile.to_string(), "acp".into()]
        };
        Self {
            name: "hermes".into(),
            bin: bin.unwrap_or("hermes").to_string(),
            args,
            env: None,
            title: Some("Hermes".into()),
        }
    }

    /// Create a Reasonix ACP runtime config.
    /// `wrapper_path` is relative to the project root (where Cargo.toml's parent is).
    /// Default: `design/05-editor-mainline/reference-code/DeepSeek-Reasonix-main/scripts/reasonix-acp-wrapper.mjs` (dev),
    /// or in production, the absolute path is resolved via `CARGO_MANIFEST_DIR` (the `rust/` directory).
    pub fn reasonix(wrapper_path: Option<&str>) -> Self {
        // CARGO_MANIFEST_DIR is the directory containing this crate's Cargo.toml:
        //   /mnt/Data1T/mnote/rust/crates/mnote-web/
        // We need the project root: /mnt/Data1T/mnote/
        let project_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .ancestors()
            .nth(3) // up: mnote-web/ → crates/ → rust/ → mnote/ (project root)
            .unwrap_or(std::path::Path::new("/mnt/Data1T/mnote"))
            .to_string_lossy()
            .to_string();
        let default_path = format!("{project_root}/scripts/reasonix-acp-wrapper.mjs");
        // Resolve wrapper_path: if it's relative, prepend project_root; absolute paths used as-is
        let resolved_path = wrapper_path
            .map(|p| {
                if p.starts_with('/') {
                    p.to_string()
                } else {
                    format!("{project_root}/{p}")
                }
            })
            .unwrap_or(default_path);
        Self {
            name: "reasonix".into(),
            bin: "node".into(),
            args: vec![resolved_path],
            env: None,
            title: Some("Reasonix".into()),
        }
    }
}

// ── Runtime Manager ──────────────────────────────────

/// Manages lifecycle of multiple agent runtimes.
///
/// Each runtime is defined by a name and spawn configuration.
/// At most one runtime is "active" at a time, providing an [`AcpClient`].
#[derive(Debug)]
pub struct AcpRuntimeManager {
    runtimes: HashMap<String, AcpRuntimeConfig>,
    active: Mutex<Option<ActiveRuntime>>,
    default_runtime: String,
}

#[derive(Debug)]
struct ActiveRuntime {
    config: AcpRuntimeConfig,
    client: Arc<AcpClient>,
}

impl AcpRuntimeManager {
    /// Create a new runtime manager with built-in default configurations.
    ///
    /// Reads environment variables to configure Hermes and Reasonix runtimes.
    /// Default active runtime is set by `MNOTE_WEB_ACP_DEFAULT_RUNTIME` (default: "reasonix").
    pub fn from_env() -> Self {
        let mut runtimes: HashMap<String, AcpRuntimeConfig> = HashMap::new();

        // Check for JSON-based configuration first
        if let Ok(json) = env::var("MNOTE_WEB_ACP_RUNTIMES") {
            if let Ok(custom_runtimes) = serde_json::from_str::<Vec<AcpRuntimeConfig>>(&json) {
                for rt in custom_runtimes {
                    let name = rt.name.clone();
                    runtimes.insert(name, rt);
                }
            } else {
                warn!("Failed to parse MNOTE_WEB_ACP_RUNTIMES JSON");
            }
        }

        // Always add default Hermes if not already configured
        if !runtimes.contains_key("hermes") {
            let hermes_bin = env::var("MNOTE_WEB_HERMES_BIN").unwrap_or_else(|_| "hermes".into());
            let hermes_profile =
                env::var("MNOTE_WEB_HERMES_ACP_PROFILE").unwrap_or_else(|_| "default".into());
            runtimes.insert(
                "hermes".into(),
                AcpRuntimeConfig::hermes(Some(&hermes_bin), Some(&hermes_profile)),
            );
        }

        // Always add default Reasonix if not already configured
        if !runtimes.contains_key("reasonix") {
            let wrapper = env::var("MNOTE_WEB_REASONIX_WRAPPER")
                .unwrap_or_else(|_| "scripts/reasonix-acp-wrapper.mjs".into());
            runtimes.insert(
                "reasonix".into(),
                AcpRuntimeConfig::reasonix(Some(&wrapper)),
            );
        }

        let default =
            env::var("MNOTE_WEB_ACP_DEFAULT_RUNTIME").unwrap_or_else(|_| "reasonix".into());

        Self {
            runtimes,
            active: Mutex::new(None),
            default_runtime: default,
        }
    }

    /// Get the list of available runtime names.
    pub fn available_runtimes(&self) -> Vec<String> {
        self.runtimes.keys().cloned().collect()
    }

    /// Get a runtime config by name.
    pub fn get_config(&self, name: &str) -> Option<&AcpRuntimeConfig> {
        self.runtimes.get(name)
    }

    /// Get the default runtime name.
    pub fn default_runtime(&self) -> &str {
        &self.default_runtime
    }

    /// Get the currently active runtime name, if any.
    pub async fn active_runtime_name(&self) -> Option<String> {
        self.active
            .lock()
            .await
            .as_ref()
            .map(|a| a.config.name.clone())
    }

    /// Get a reference to the currently active [`AcpClient`], if any.
    pub async fn active_client(&self) -> Option<Arc<AcpClient>> {
        self.active.lock().await.as_ref().map(|a| a.client.clone())
    }

    /// Check if a runtime is active and the client is available.
    pub async fn is_active(&self) -> bool {
        self.active.lock().await.is_some()
    }

    /// Activate a runtime by name, spawning a new subprocess if needed.
    ///
    /// If another runtime is currently active, it will be shut down first.
    /// After spawn, performs an `initialize` handshake to verify the runtime is healthy.
    pub async fn switch_to(&self, name: &str) -> Result<Arc<AcpClient>, AcpError> {
        let config = self
            .runtimes
            .get(name)
            .cloned()
            .ok_or_else(|| AcpError::Internal(format!("unknown runtime: {name}")))?;
        self.switch_to_config(config).await
    }

    /// Activate a runtime from an explicit config.
    ///
    /// This is used by Hermes ACP because the binary is the same runtime name,
    /// but the selected Hermes profile changes the launch args.
    pub async fn switch_to_config(
        &self,
        config: AcpRuntimeConfig,
    ) -> Result<Arc<AcpClient>, AcpError> {
        let name = config.name.clone();
        // Shutdown current active runtime
        let mut active_guard = self.active.lock().await;
        if let Some(ref current) = *active_guard {
            if current.config == config {
                // Already active — return existing client
                return Ok(current.client.clone());
            }
            // Drop the old ActiveRuntime, which will kill the child process
            // (via AcpClient's Drop impl)
        }

        info!(
            "ACP runtime: switching to {name} (bin={}, args={:?})",
            config.bin, config.args
        );

        // Convert Vec<String> to Vec<&str> for AcpClient::spawn
        let args_refs: Vec<&str> = config.args.iter().map(|s| s.as_str()).collect();
        let client =
            AcpClient::spawn_with_env(&config.bin, &args_refs, config.env.as_ref()).await?;
        let client = Arc::new(client);

        *active_guard = Some(ActiveRuntime {
            config,
            client: client.clone(),
        });

        info!("ACP runtime: {name} active");
        Ok(client)
    }

    /// Shut down the currently active runtime.
    pub async fn shutdown_active(&self) {
        let mut active_guard = self.active.lock().await;
        if let Some(active) = active_guard.take() {
            info!("ACP runtime: shutting down {}", active.config.name);
            // AcpClient's Drop kills the process
        }
    }

    /// Perform a health check on the active runtime.
    ///
    /// Returns `true` if the runtime responds to an `initialize` handshake within 5 seconds.
    pub async fn health_check(&self) -> bool {
        let client = match self.active_client().await {
            Some(c) => c,
            None => return false,
        };

        // Use request_with_timeout with a short timeout
        let result: Result<serde_json::Value, AcpError> = timeout(
            Duration::from_secs(5),
            client.request("initialize", serde_json::json!({ "protocolVersion": 1 })),
        )
        .await
        .map_err(|_| AcpError::Timeout(5))
        .and_then(|r| r);

        match result {
            Ok(val) => {
                let ok = val.get("protocolVersion").and_then(|v| v.as_u64()) == Some(1);
                if ok {
                    debug!("ACP health check OK");
                } else {
                    warn!("ACP health check: unexpected response: {val:?}");
                }
                ok
            }
            Err(e) => {
                warn!("ACP health check failed: {e}");
                false
            }
        }
    }
}

impl Drop for AcpRuntimeManager {
    fn drop(&mut self) {
        // The active runtime's AcpClient Drop will kill the process
    }
}

// ── Tests ────────────────────────────────────────────

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

    #[test]
    fn test_runtime_config_hermes() {
        let cfg = AcpRuntimeConfig::hermes(None, None);
        assert_eq!(cfg.name, "hermes");
        assert_eq!(cfg.bin, "hermes");
        assert_eq!(cfg.args, vec!["-p", "default", "acp"]);
    }

    #[test]
    fn test_runtime_config_hermes_profile_can_be_disabled() {
        let cfg = AcpRuntimeConfig::hermes(None, Some(""));
        assert_eq!(cfg.args, vec!["acp"]);
    }

    #[test]
    fn test_runtime_config_reasonix() {
        let cfg = AcpRuntimeConfig::reasonix(None);
        assert_eq!(cfg.name, "reasonix");
        assert_eq!(cfg.bin, "node");
        assert_eq!(cfg.args.len(), 1);
        assert!(cfg.args[0].ends_with("/scripts/reasonix-acp-wrapper.mjs"));
    }

    #[test]
    fn test_runtime_config_custom() {
        let cfg = AcpRuntimeConfig {
            name: "custom".into(),
            bin: "/usr/local/bin/my-agent".into(),
            args: vec!["--acp".into(), "--debug".into()],
            env: None,
            title: Some("My Agent".into()),
        };
        let json = serde_json::to_value(&cfg).unwrap();
        assert_eq!(json["name"], "custom");
        assert_eq!(json["bin"], "/usr/local/bin/my-agent");
        assert_eq!(json["title"], "My Agent");
    }

    #[test]
    fn test_runtime_manager_from_env_defaults() {
        // Without env overrides, should contain hermes and reasonix
        let mgr = AcpRuntimeManager::from_env();
        let runtimes = mgr.available_runtimes();
        assert!(runtimes.contains(&"hermes".into()));
        assert!(runtimes.contains(&"reasonix".into()));
    }

    #[tokio::test]
    async fn test_switch_to_unknown_runtime() {
        let mgr = AcpRuntimeManager::from_env();
        let result = mgr.switch_to("nonexistent").await;
        assert!(result.is_err());
        let err_str = format!("{}", result.err().unwrap());
        assert!(
            err_str.contains("unknown runtime"),
            "should return error for unknown runtime, got: {err_str}"
        );
    }

    #[tokio::test]
    async fn test_health_check_no_active() {
        let mgr = AcpRuntimeManager::from_env();
        assert!(!mgr.health_check().await, "no active runtime = unhealthy");
    }

    #[tokio::test]
    async fn test_switch_to_hermes_requires_binary() {
        let mgr = AcpRuntimeManager::from_env();
        // This might fail if `hermes` binary is not in PATH — that's OK for this test
        let result = mgr.switch_to("hermes").await;
        // We just verify it doesn't panic; either succeeds or returns Spawn error
        if let Err(e) = &result {
            assert!(
                matches!(e, AcpError::Spawn(_)),
                "expected Spawn error if hermes not in PATH, got: {e}"
            );
        } else {
            // Success — clean up
            mgr.shutdown_active().await;
        }
    }
}
