use super::sqlite::SqliteMemory;
use super::traits::{Memory, MemoryCategory, MemoryEntry, normalize_recent_recall_query};
use anyhow::Context;
use async_trait::async_trait;
use chrono::Local;
use parking_lot::Mutex;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::io::AsyncReadExt;
use tokio::process::Command;
use tokio::time::timeout;

pub struct LucidMemory {
    alias: String,
    local: SqliteMemory,
    lucid_cmd: String,
    token_budget: usize,
    workspace_dir: PathBuf,
    recall_timeout: Duration,
    store_timeout: Duration,
    local_hit_threshold: usize,
    failure_cooldown: Duration,
    last_failure_at: Mutex<Option<Instant>>,
}

impl LucidMemory {
    const DEFAULT_LUCID_CMD: &'static str = "lucid";
    const DEFAULT_TOKEN_BUDGET: usize = 200;
    // Lucid CLI cold starts include loading the local embedding model and can
    // exceed 1.5s on ARM hosts. Keep the bound finite while avoiding fallback
    // before a healthy process has had time to initialize.
    const DEFAULT_RECALL_TIMEOUT_MS: u64 = 3_000;
    const DEFAULT_STORE_TIMEOUT_MS: u64 = 3_000;
    const DEFAULT_LOCAL_HIT_THRESHOLD: usize = 3;
    const DEFAULT_FAILURE_COOLDOWN_MS: u64 = 15_000;

    pub fn new(alias: &str, workspace_dir: &Path, local: SqliteMemory) -> Self {
        Self::with_overrides(alias, workspace_dir, local, None, None, None)
    }

    /// Construct with config-sourced overrides (`[storage.lucid.<alias>]`).
    /// Each `None` falls back to the built-in default, so an unconfigured
    /// alias behaves exactly like [`Self::new`]. Explicit blank commands stay
    /// invalid instead of falling through to `PATH`; zero deadlines fall back
    /// because normal config loading warns and keeps the process available for
    /// operator repair after validation errors.
    pub fn with_overrides(
        alias: &str,
        workspace_dir: &Path,
        local: SqliteMemory,
        lucid_cmd: Option<String>,
        recall_timeout_ms: Option<u64>,
        store_timeout_ms: Option<u64>,
    ) -> Self {
        let lucid_cmd = lucid_cmd.unwrap_or_else(|| Self::DEFAULT_LUCID_CMD.to_string());
        let recall_timeout_ms = recall_timeout_ms
            .filter(|timeout| *timeout > 0)
            .unwrap_or(Self::DEFAULT_RECALL_TIMEOUT_MS);
        let store_timeout_ms = store_timeout_ms
            .filter(|timeout| *timeout > 0)
            .unwrap_or(Self::DEFAULT_STORE_TIMEOUT_MS);

        Self {
            alias: alias.to_string(),
            local,
            lucid_cmd,
            token_budget: Self::DEFAULT_TOKEN_BUDGET,
            workspace_dir: workspace_dir.to_path_buf(),
            recall_timeout: Duration::from_millis(recall_timeout_ms),
            store_timeout: Duration::from_millis(store_timeout_ms),
            local_hit_threshold: Self::DEFAULT_LOCAL_HIT_THRESHOLD,
            failure_cooldown: Duration::from_millis(Self::DEFAULT_FAILURE_COOLDOWN_MS),
            last_failure_at: Mutex::new(None),
        }
    }

    #[cfg(all(test, unix))]
    #[allow(clippy::too_many_arguments)]
    fn with_options(
        alias: &str,
        workspace_dir: &Path,
        local: SqliteMemory,
        lucid_cmd: String,
        token_budget: usize,
        local_hit_threshold: usize,
        recall_timeout: Duration,
        store_timeout: Duration,
        failure_cooldown: Duration,
    ) -> Self {
        Self {
            alias: alias.to_string(),
            local,
            lucid_cmd,
            token_budget,
            workspace_dir: workspace_dir.to_path_buf(),
            recall_timeout,
            store_timeout,
            local_hit_threshold: local_hit_threshold.max(1),
            failure_cooldown,
            last_failure_at: Mutex::new(None),
        }
    }

    #[cfg(all(test, unix))]
    pub(super) fn test_process_config(&self) -> (&str, Duration, Duration) {
        (&self.lucid_cmd, self.recall_timeout, self.store_timeout)
    }

    fn in_failure_cooldown(&self) -> bool {
        let guard = self.last_failure_at.lock();
        guard
            .as_ref()
            .is_some_and(|last| last.elapsed() < self.failure_cooldown)
    }

    fn mark_failure_now(&self) {
        let mut guard = self.last_failure_at.lock();
        *guard = Some(Instant::now());
    }

    fn clear_failure(&self) {
        let mut guard = self.last_failure_at.lock();
        *guard = None;
    }

    fn to_lucid_type(category: &MemoryCategory) -> &'static str {
        match category {
            MemoryCategory::Core => "decision",
            MemoryCategory::Daily => "context",
            MemoryCategory::Conversation => "conversation",
            MemoryCategory::Custom(_) => "learning",
        }
    }

    fn to_memory_category(label: &str) -> MemoryCategory {
        let normalized = label.to_lowercase();
        if normalized.contains("visual") {
            return MemoryCategory::Custom("visual".to_string());
        }

        match normalized.as_str() {
            "decision" | "learning" | "solution" => MemoryCategory::Core,
            "context" | "conversation" => MemoryCategory::Conversation,
            "bug" => MemoryCategory::Daily,
            other => MemoryCategory::Custom(other.to_string()),
        }
    }

    fn merge_results(
        primary_results: Vec<MemoryEntry>,
        secondary_results: Vec<MemoryEntry>,
        limit: usize,
    ) -> Vec<MemoryEntry> {
        if limit == 0 {
            return Vec::new();
        }

        let mut merged = Vec::new();
        let mut seen = HashSet::new();

        for entry in primary_results.into_iter().chain(secondary_results) {
            let signature = format!(
                "{}\u{0}{}",
                entry.key.to_lowercase(),
                entry.content.to_lowercase()
            );

            if seen.insert(signature) {
                merged.push(entry);
                if merged.len() >= limit {
                    break;
                }
            }
        }

        merged
    }

    fn parse_lucid_context(raw: &str) -> Vec<MemoryEntry> {
        let mut in_context_block = false;
        let mut entries = Vec::new();
        let now = Local::now().to_rfc3339();

        for line in raw.lines().map(str::trim) {
            if line == "<lucid-context>" {
                in_context_block = true;
                continue;
            }

            if line == "</lucid-context>" {
                break;
            }

            if !in_context_block || line.is_empty() {
                continue;
            }

            let Some(rest) = line.strip_prefix("- [") else {
                continue;
            };

            let Some((label, content_part)) = rest.split_once(']') else {
                continue;
            };

            let content = content_part.trim();
            if content.is_empty() {
                continue;
            }

            let rank = entries.len();
            entries.push(MemoryEntry {
                id: format!("lucid:{rank}"),
                key: format!("lucid_{rank}"),
                content: content.to_string(),
                category: Self::to_memory_category(label.trim()),
                timestamp: now.clone(),
                session_id: None,
                score: Some((1.0 - rank as f64 * 0.05).max(0.1)),
                namespace: "default".into(),
                importance: None,
                superseded_by: None,
                kind: None,
                pinned: false,
                tenant_id: None,
                agent_alias: None,
                agent_id: None,
            });
        }

        entries
    }

    async fn run_lucid_command_raw(
        lucid_cmd: &str,
        args: &[String],
        timeout_window: Duration,
    ) -> anyhow::Result<String> {
        let mut cmd = Command::new(lucid_cmd);
        cmd.args(args)
            .kill_on_drop(true)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());

        let child = cmd
            .spawn()
            .with_context(|| format!("failed to spawn lucid command {lucid_cmd:?}"))?;

        Self::wait_for_lucid_child(child, lucid_cmd, timeout_window).await
    }

    async fn wait_for_lucid_child(
        mut child: tokio::process::Child,
        lucid_cmd: &str,
        timeout_window: Duration,
    ) -> anyhow::Result<String> {
        let mut stdout = child
            .stdout
            .take()
            .context("lucid command stdout pipe was not captured")?;
        let mut stderr = child
            .stderr
            .take()
            .context("lucid command stderr pipe was not captured")?;
        let mut stdout_bytes = Vec::new();
        let mut stderr_bytes = Vec::new();

        let output = timeout(timeout_window, async {
            let (status, stdout_result, stderr_result) = tokio::join!(
                child.wait(),
                stdout.read_to_end(&mut stdout_bytes),
                stderr.read_to_end(&mut stderr_bytes),
            );
            stdout_result.context("failed to read lucid command stdout")?;
            stderr_result.context("failed to read lucid command stderr")?;
            status.context("failed to wait for lucid command")
        })
        .await;

        let status = match output {
            Ok(status) => status?,
            Err(_) => {
                let cleanup_error = child.kill().await.err().map(|error| error.to_string());
                ::zeroclaw_log::record!(
                    ERROR,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Timeout)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({
                            "command": lucid_cmd,
                            "timeout_ms": timeout_window.as_millis() as u64,
                            "cleanup_error": cleanup_error,
                        })),
                    "lucid command timed out"
                );
                if let Some(cleanup_error) = cleanup_error {
                    anyhow::bail!(
                        "lucid command timed out after {}ms; failed to terminate and reap child: {cleanup_error}",
                        timeout_window.as_millis()
                    );
                }
                anyhow::bail!(
                    "lucid command timed out after {}ms",
                    timeout_window.as_millis()
                );
            }
        };

        if !status.success() {
            let stderr = String::from_utf8_lossy(&stderr_bytes);
            anyhow::bail!("lucid command failed: {stderr}");
        }

        Ok(String::from_utf8_lossy(&stdout_bytes).to_string())
    }

    async fn run_lucid_command(
        &self,
        args: &[String],
        timeout_window: Duration,
    ) -> anyhow::Result<String> {
        Self::run_lucid_command_raw(&self.lucid_cmd, args, timeout_window).await
    }

    fn build_store_args(&self, key: &str, content: &str, category: &MemoryCategory) -> Vec<String> {
        let payload = format!("{key}: {content}");
        vec![
            "store".to_string(),
            payload,
            format!("--type={}", Self::to_lucid_type(category)),
            format!("--project={}", self.workspace_dir.display().to_string()),
        ]
    }

    fn build_recall_args(&self, query: &str) -> Vec<String> {
        vec![
            "context".to_string(),
            query.to_string(),
            format!("--budget={}", self.token_budget),
            format!("--project={}", self.workspace_dir.display().to_string()),
        ]
    }

    async fn sync_to_lucid_async(&self, key: &str, content: &str, category: &MemoryCategory) {
        let args = self.build_store_args(key, content, category);
        if let Err(error) = self.run_lucid_command(&args, self.store_timeout).await {
            ::zeroclaw_log::record!(
                DEBUG,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                    .with_attrs(
                        ::serde_json::json!({"command": self.lucid_cmd, "error": format!("{}", error)})
                    ),
                "Lucid store sync failed; sqlite remains authoritative"
            );
        }
    }

    async fn recall_from_lucid(&self, query: &str) -> anyhow::Result<Vec<MemoryEntry>> {
        let args = self.build_recall_args(query);
        let output = self.run_lucid_command(&args, self.recall_timeout).await?;
        Ok(Self::parse_lucid_context(&output))
    }

    /// Dimensions of the underlying local SQLite embedder (0 = Noop). Lets
    /// callers confirm a live embedder refresh reached the local backend
    pub fn embedder_dimensions(&self) -> usize {
        self.local.embedder_dimensions()
    }
}

#[async_trait]
impl Memory for LucidMemory {
    fn name(&self) -> &str {
        "lucid"
    }

    fn refresh_embedder(
        &self,
        model_provider: &str,
        api_key: Option<&str>,
        model: &str,
        dimensions: usize,
    ) {
        // Lucid delegates all local storage/embedding to the wrapped SQLite
        // backend, so forward the refresh there Without this, a
        // Lucid-backed handle (including the install-wide RPC handle when
        // `backend = lucid`) would keep a stale embedder.
        self.local
            .refresh_embedder(model_provider, api_key, model, dimensions);
    }

    async fn store(
        &self,
        key: &str,
        content: &str,
        category: MemoryCategory,
        session_id: Option<&str>,
    ) -> anyhow::Result<()> {
        self.local
            .store(key, content, category.clone(), session_id)
            .await?;
        self.sync_to_lucid_async(key, content, &category).await;
        Ok(())
    }

    async fn recall(
        &self,
        query: &str,
        limit: usize,
        session_id: Option<&str>,
        since: Option<&str>,
        until: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>> {
        let since_dt = since
            .map(chrono::DateTime::parse_from_rfc3339)
            .transpose()
            .map_err(|e| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(
                            ::serde_json::json!({"field": "since", "error": format!("{}", e)})
                        ),
                    "recall window bound rejected"
                );
                anyhow::Error::msg(format!("invalid 'since' date (expected RFC 3339): {e}"))
            })?;
        let until_dt = until
            .map(chrono::DateTime::parse_from_rfc3339)
            .transpose()
            .map_err(|e| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(
                            ::serde_json::json!({"field": "until", "error": format!("{}", e)})
                        ),
                    "recall window bound rejected"
                );
                anyhow::Error::msg(format!("invalid 'until' date (expected RFC 3339): {e}"))
            })?;
        if let (Some(s), Some(u)) = (&since_dt, &until_dt)
            && s >= u
        {
            anyhow::bail!("'since' must be before 'until'");
        }

        let recall_query = normalize_recent_recall_query(query);

        let local_results = self
            .local
            .recall(recall_query, limit, session_id, since, until)
            .await?;
        if limit == 0
            || local_results.len() >= limit
            || local_results.len() >= self.local_hit_threshold
        {
            return Ok(local_results);
        }

        if self.in_failure_cooldown() {
            return Ok(local_results);
        }

        match self.recall_from_lucid(recall_query).await {
            Ok(lucid_results) if !lucid_results.is_empty() => {
                self.clear_failure();
                let merged = Self::merge_results(local_results, lucid_results, limit);
                let filtered: Vec<MemoryEntry> = merged
                    .into_iter()
                    .filter(|e| {
                        if let Some(ref s) = since_dt
                            && let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&e.timestamp)
                            && ts < *s
                        {
                            return false;
                        }
                        if let Some(ref u) = until_dt
                            && let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&e.timestamp)
                            && ts > *u
                        {
                            return false;
                        }
                        true
                    })
                    .collect();
                Ok(filtered)
            }
            Ok(_) => {
                self.clear_failure();
                Ok(local_results)
            }
            Err(error) => {
                self.mark_failure_now();
                ::zeroclaw_log::record!(DEBUG, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(::serde_json::json!({"command": self.lucid_cmd, "error": format!("{}", error)})), "Lucid context unavailable; using local sqlite results");
                Ok(local_results)
            }
        }
    }

    async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>> {
        self.local.get(key).await
    }

    async fn get_for_agent(
        &self,
        key: &str,
        agent_id: &str,
    ) -> anyhow::Result<Option<MemoryEntry>> {
        self.local.get_for_agent(key, agent_id).await
    }

    async fn list(
        &self,
        category: Option<&MemoryCategory>,
        session_id: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>> {
        self.local.list(category, session_id).await
    }

    async fn forget(&self, key: &str) -> anyhow::Result<bool> {
        self.local.forget(key).await
    }

    async fn forget_for_agent(&self, key: &str, agent_id: &str) -> anyhow::Result<bool> {
        self.local.forget_for_agent(key, agent_id).await
    }

    async fn purge_session_for_agent(
        &self,
        session_id: &str,
        agent_id: &str,
    ) -> anyhow::Result<usize> {
        self.local
            .purge_session_for_agent(session_id, agent_id)
            .await
    }

    async fn purge_agent(&self, agent_alias: &str) -> anyhow::Result<usize> {
        self.local.purge_agent(agent_alias).await
    }

    async fn export_agent(&self, agent_alias: &str) -> anyhow::Result<Vec<MemoryEntry>> {
        self.local.export_agent(agent_alias).await
    }

    async fn rename_agent(&self, from: &str, to: &str) -> anyhow::Result<usize> {
        // The remote Lucid daemon has no agent_id concept; alias→UUID lives in
        // the local SQLite mirror, so rename is a local update (mirrors purge).
        self.local.rename_agent(from, to).await
    }

    async fn count_agent(&self, agent_alias: &str) -> anyhow::Result<usize> {
        // Attribution lives only on the local SQLite mirror (see rename_agent).
        self.local.count_agent(agent_alias).await
    }

    async fn count(&self) -> anyhow::Result<usize> {
        self.local.count().await
    }

    async fn health_check(&self) -> bool {
        self.local.health_check().await
    }

    async fn store_with_agent(
        &self,
        key: &str,
        content: &str,
        category: MemoryCategory,
        session_id: Option<&str>,
        namespace: Option<&str>,
        importance: Option<f64>,
        agent_id: Option<&str>,
    ) -> anyhow::Result<()> {
        // Lucid composes a local SqliteMemory + a remote Lucid daemon; the
        // remote side has no agent_id concept, so the attribution lives
        // only on the local SQLite mirror. The async sync to the daemon
        // continues unchanged.
        self.local
            .store_with_agent(
                key,
                content,
                category.clone(),
                session_id,
                namespace,
                importance,
                agent_id,
            )
            .await?;
        self.sync_to_lucid_async(key, content, &category).await;
        Ok(())
    }

    async fn recall_for_agents(
        &self,
        allowed_agent_ids: &[&str],
        query: &str,
        limit: usize,
        session_id: Option<&str>,
        since: Option<&str>,
        until: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>> {
        // Lucid's remote-daemon recall has no agent_id awareness; the
        // cross-agent allowlist is enforced on the local SQLite mirror
        // only. If the local hits clear the threshold the remote leg
        // never runs (matching `recall`'s short-circuit semantics).
        self.local
            .recall_for_agents(allowed_agent_ids, query, limit, session_id, since, until)
            .await
    }

    async fn ensure_agent_uuid(&self, alias: &str) -> anyhow::Result<String> {
        self.local.ensure_agent_uuid(alias).await
    }
}

impl ::zeroclaw_api::attribution::Attributable for LucidMemory {
    fn role(&self) -> ::zeroclaw_api::attribution::Role {
        ::zeroclaw_api::attribution::Role::Memory(::zeroclaw_api::attribution::MemoryKind::Lucid)
    }
    fn alias(&self) -> &str {
        &self.alias
    }
}

#[cfg(test)]
mod platform_tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn with_overrides_keeps_blank_command_invalid_and_normalizes_zero_timeouts() {
        let tmp = TempDir::new().unwrap();
        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_overrides(
            "test",
            tmp.path(),
            sqlite,
            Some("   ".into()),
            Some(0),
            Some(0),
        );

        assert_eq!(
            memory.lucid_cmd, "   ",
            "an explicit invalid selector must not fall through to PATH"
        );
        assert_eq!(
            memory.recall_timeout,
            Duration::from_millis(LucidMemory::DEFAULT_RECALL_TIMEOUT_MS)
        );
        assert_eq!(
            memory.store_timeout,
            Duration::from_secs(3),
            "the production store timeout must retain the documented cold-start allowance"
        );
    }
}

#[cfg(all(test, unix))]
mod tests {
    use super::*;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use tempfile::TempDir;

    #[test]
    fn refresh_embedder_forwards_to_local_sqlite() {
        let tmp = TempDir::new().unwrap();
        let local = SqliteMemory::new("test", tmp.path()).unwrap();
        let lucid = LucidMemory::new("lucid", tmp.path(), local);
        assert_eq!(lucid.embedder_dimensions(), 0);

        Memory::refresh_embedder(
            &lucid,
            "openai",
            Some("sk-test"),
            "text-embedding-3-small",
            1536,
        );

        assert_eq!(
            lucid.embedder_dimensions(),
            1536,
            "LucidMemory must forward refresh_embedder to the local SQLite backend"
        );
    }

    fn write_fake_lucid_script(dir: &Path) -> String {
        let script_path = dir.join("fake-lucid.sh");
        let script = r#"#!/bin/sh
set -eu

if [ "${1:-}" = "store" ]; then
  echo '{"success":true,"id":"mem_1"}'
  exit 0
fi

if [ "${1:-}" = "context" ]; then
  cat <<'EOF'
<lucid-context>
Auth context snapshot
- [decision] Use token refresh middleware
- [context] Working in src/auth.rs
</lucid-context>
EOF
  exit 0
fi

echo "unsupported command" >&2
exit 1
"#;

        fs::write(&script_path, script).unwrap();
        let mut perms = fs::metadata(&script_path).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&script_path, perms).unwrap();
        script_path.display().to_string()
    }

    fn write_delayed_lucid_script(dir: &Path) -> String {
        let script_path = dir.join("delayed-lucid.sh");
        let script = r#"#!/bin/sh
set -eu

if [ "${1:-}" = "store" ]; then
  echo '{"success":true,"id":"mem_1"}'
  exit 0
fi

if [ "${1:-}" = "context" ]; then
  # Simulate an ARM cold start that exceeds the previous 500ms timeout.
  sleep 1.5
  cat <<'EOF'
<lucid-context>
- [decision] Delayed token refresh guidance
</lucid-context>
EOF
  exit 0
fi

echo "unsupported command" >&2
exit 1
"#;

        fs::write(&script_path, script).unwrap();
        let mut perms = fs::metadata(&script_path).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&script_path, perms).unwrap();
        script_path.display().to_string()
    }

    fn write_delayed_store_lucid_script(dir: &Path, marker_path: &Path) -> String {
        let script_path = dir.join("delayed-store-lucid.sh");
        let script = format!(
            r#"#!/bin/sh
set -eu

if [ "${{1:-}}" = "store" ]; then
  sleep 1.5
  printf 'completed\n' > "{}"
  printf '{{"success":true,"id":"mem_1"}}\n'
  exit 0
fi

if [ "${{1:-}}" = "context" ]; then
  printf '<lucid-context>\n</lucid-context>\n'
  exit 0
fi

echo "unsupported command" >&2
exit 1
"#,
            marker_path.display()
        );

        fs::write(&script_path, script).unwrap();
        let mut perms = fs::metadata(&script_path).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&script_path, perms).unwrap();
        script_path.display().to_string()
    }

    fn write_probe_lucid_script(dir: &Path, marker_path: &Path) -> String {
        let script_path = dir.join("probe-lucid.sh");
        let marker = marker_path.display().to_string();
        let script = format!(
            r#"#!/bin/sh
set -eu

if [ "${{1:-}}" = "store" ]; then
  echo '{{"success":true,"id":"mem_store"}}'
  exit 0
fi

if [ "${{1:-}}" = "context" ]; then
  printf 'context\n' >> "{marker}"
  cat <<'EOF'
<lucid-context>
- [decision] should not be used when local hits are enough
</lucid-context>
EOF
  exit 0
fi

echo "unsupported command" >&2
exit 1
"#
        );

        fs::write(&script_path, script).unwrap();
        let mut perms = fs::metadata(&script_path).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&script_path, perms).unwrap();
        script_path.display().to_string()
    }

    fn test_memory(workspace: &Path, cmd: String) -> LucidMemory {
        let sqlite = SqliteMemory::new("sqlite", workspace).unwrap();
        LucidMemory::with_options(
            "test",
            workspace,
            sqlite,
            cmd,
            200,
            3,
            Duration::from_secs(5),
            Duration::from_secs(5),
            Duration::from_secs(2),
        )
    }

    #[tokio::test]
    async fn lucid_name() {
        let tmp = TempDir::new().unwrap();
        let memory = test_memory(tmp.path(), "nonexistent-lucid-binary".to_string());
        assert_eq!(memory.name(), "lucid");
    }

    #[tokio::test]
    async fn store_succeeds_when_lucid_missing() {
        let tmp = TempDir::new().unwrap();
        let memory = test_memory(tmp.path(), "nonexistent-lucid-binary".to_string());

        memory
            .store("lang", "User prefers Rust", MemoryCategory::Core, None)
            .await
            .unwrap();

        let entry = memory.get("lang").await.unwrap();
        assert!(entry.is_some());
        assert_eq!(entry.unwrap().content, "User prefers Rust");
    }

    #[tokio::test]
    async fn recall_merges_lucid_and_local_results() {
        let tmp = TempDir::new().unwrap();
        let fake_cmd = write_fake_lucid_script(tmp.path());
        let memory = test_memory(tmp.path(), fake_cmd);

        memory
            .store(
                "local_note",
                "Local sqlite auth fallback note",
                MemoryCategory::Core,
                None,
            )
            .await
            .unwrap();

        let entries = memory.recall("auth", 5, None, None, None).await.unwrap();

        assert!(
            entries
                .iter()
                .any(|e| e.content.contains("Local sqlite auth fallback note"))
        );
        assert!(entries.iter().any(|e| e.content.contains("token refresh")));
    }

    #[tokio::test]
    async fn recall_waits_for_delayed_lucid_process_with_test_timeout() {
        let tmp = TempDir::new().unwrap();
        let delayed_cmd = write_delayed_lucid_script(tmp.path());
        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_options(
            "test",
            tmp.path(),
            sqlite,
            delayed_cmd,
            200,
            3,
            Duration::from_secs(10),
            Duration::from_secs(10),
            Duration::from_secs(2),
        );

        memory
            .store(
                "local_note",
                "Local sqlite auth fallback note",
                MemoryCategory::Core,
                None,
            )
            .await
            .unwrap();

        let entries = memory.recall("auth", 5, None, None, None).await.unwrap();

        assert!(
            entries
                .iter()
                .any(|e| e.content.contains("Local sqlite auth fallback note"))
        );
        assert!(
            entries
                .iter()
                .any(|e| e.content.contains("Delayed token refresh guidance"))
        );
    }

    #[tokio::test]
    async fn store_waits_for_delayed_lucid_process_with_test_timeout() {
        let tmp = TempDir::new().unwrap();
        let marker_path = tmp.path().join("store-completed.marker");
        let delayed_cmd = write_delayed_store_lucid_script(tmp.path(), &marker_path);
        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_options(
            "test",
            tmp.path(),
            sqlite,
            delayed_cmd,
            200,
            3,
            Duration::from_secs(10),
            Duration::from_secs(10),
            Duration::from_secs(2),
        );

        memory
            .store(
                "cold_start",
                "Store survives Lucid cold start",
                MemoryCategory::Core,
                None,
            )
            .await
            .unwrap();

        assert!(
            marker_path.exists(),
            "the store path must wait for a healthy delayed Lucid process"
        );
    }

    #[tokio::test]
    async fn timeout_terminates_and_reaps_lucid_child() {
        let mut command = Command::new("sleep");
        command
            .arg("10")
            .kill_on_drop(true)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());
        let child = command.spawn().expect("sleep command must start");
        let pid = child.id().expect("spawned sleep command must have a PID");
        let timeout = Duration::from_millis(100);

        let error = LucidMemory::wait_for_lucid_child(child, "sleep", timeout)
            .await
            .expect_err("delayed command must time out");
        assert!(
            error
                .to_string()
                .contains(&format!("timed out after {}ms", timeout.as_millis()))
        );

        let still_running = std::process::Command::new("kill")
            .args(["-0", &pid.to_string()])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .expect("kill -0 must be available on Unix")
            .success();
        assert!(
            !still_running,
            "timed-out Lucid child PID {pid} still exists"
        );
    }

    #[test]
    fn with_overrides_none_matches_new_defaults() {
        let tmp = TempDir::new().unwrap();
        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_overrides(
            "test",
            tmp.path(),
            sqlite,
            Some("lucid-test".to_string()),
            None,
            None,
        );

        assert_eq!(
            memory.recall_timeout,
            Duration::from_millis(LucidMemory::DEFAULT_RECALL_TIMEOUT_MS)
        );
        assert_eq!(
            memory.store_timeout,
            Duration::from_millis(LucidMemory::DEFAULT_STORE_TIMEOUT_MS)
        );
    }

    #[tokio::test]
    async fn with_overrides_recall_timeout_ms_is_applied() {
        let tmp = TempDir::new().unwrap();
        let delayed_cmd = write_delayed_lucid_script(tmp.path());
        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_overrides(
            "test",
            tmp.path(),
            sqlite,
            Some(delayed_cmd),
            Some(50),
            None,
        );

        memory
            .store(
                "local_note",
                "Local sqlite auth fallback note",
                MemoryCategory::Core,
                None,
            )
            .await
            .unwrap();

        let entries = memory.recall("auth", 5, None, None, None).await.unwrap();

        assert!(
            entries
                .iter()
                .any(|e| e.content.contains("Local sqlite auth fallback note")),
            "local sqlite results must still come back on Lucid timeout"
        );
        assert!(
            entries
                .iter()
                .all(|e| !e.content.contains("Delayed token refresh guidance")),
            "a 50ms recall_timeout_ms override must time out against the \
             1.5s simulated cold start, proving the override reaches the \
             running timeout rather than being ignored"
        );
    }

    #[tokio::test]
    async fn recall_skips_lucid_when_local_hits_are_enough() {
        let tmp = TempDir::new().unwrap();
        let marker = tmp.path().join("context_calls.log");
        let probe_cmd = write_probe_lucid_script(tmp.path(), &marker);

        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_options(
            "test",
            tmp.path(),
            sqlite,
            probe_cmd,
            200,
            1,
            Duration::from_secs(5),
            Duration::from_secs(5),
            Duration::from_secs(2),
        );

        memory
            .store(
                "pref",
                "Rust should stay local-first",
                MemoryCategory::Core,
                None,
            )
            .await
            .unwrap();

        let entries = memory.recall("rust", 5, None, None, None).await.unwrap();
        assert!(
            entries
                .iter()
                .any(|e| e.content.contains("Rust should stay local-first"))
        );

        let context_calls = tokio::fs::read_to_string(&marker).await.unwrap_or_default();
        assert!(
            context_calls.trim().is_empty(),
            "Expected local-hit short-circuit; got calls: {context_calls}"
        );
    }

    fn write_failing_lucid_script(dir: &Path, marker_path: &Path) -> String {
        let script_path = dir.join("failing-lucid.sh");
        let marker = marker_path.display().to_string();
        let script = format!(
            r#"#!/bin/sh
set -eu

if [ "${{1:-}}" = "store" ]; then
  echo '{{"success":true,"id":"mem_store"}}'
  exit 0
fi

if [ "${{1:-}}" = "context" ]; then
  printf 'context\n' >> "{marker}"
  echo "simulated lucid failure" >&2
  exit 1
fi

echo "unsupported command" >&2
exit 1
"#
        );

        fs::write(&script_path, script).unwrap();
        let mut perms = fs::metadata(&script_path).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&script_path, perms).unwrap();
        script_path.display().to_string()
    }

    #[tokio::test]
    async fn failure_cooldown_avoids_repeated_lucid_calls() {
        let tmp = TempDir::new().unwrap();
        let marker = tmp.path().join("failing_context_calls.log");
        let failing_cmd = write_failing_lucid_script(tmp.path(), &marker);

        let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
        let memory = LucidMemory::with_options(
            "test",
            tmp.path(),
            sqlite,
            failing_cmd,
            200,
            99,
            Duration::from_secs(5),
            Duration::from_secs(5),
            Duration::from_secs(5),
        );

        let first = memory.recall("auth", 5, None, None, None).await.unwrap();
        let second = memory.recall("auth", 5, None, None, None).await.unwrap();

        assert!(first.is_empty());
        assert!(second.is_empty());

        let calls = tokio::fs::read_to_string(&marker).await.unwrap_or_default();
        assert_eq!(calls.lines().count(), 1);
    }
}
