use std::{
    collections::BTreeMap,
    fs,
    path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};

use crate::{
    agent_template::{agent_memory_operator_path, agent_memory_self_path},
    storage::AppStorage,
    tool::helpers::{command_digest, command_preview, command_receipt_source_ref},
    types::{
        BriefRecord, CommandTaskStatusSnapshot, ContextEpisodeRecord, TaskRecord, TaskStatus,
        ToolExecutionRecord, WorkItemRecord, WorkspaceEntry,
    },
};

const INDEX_FILENAME: &str = "memory.sqlite3";
const DIRTY_FILENAME: &str = "memory.dirty";
const SEARCH_LIMIT_MAX: usize = 50;
const GET_CHARS_DEFAULT: usize = 12_000;
const GET_CHARS_MAX: usize = 50_000;
const REBUILD_BRIEF_LIMIT: usize = 500;
const REBUILD_EPISODE_LIMIT: usize = 500;
const REBUILD_WORK_ITEM_LIMIT: usize = 500;
const REBUILD_COMMAND_RECEIPT_LIMIT: usize = 500;
const REBUILD_TASK_HISTORY_LIMIT: usize = 500;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct MemorySearchResult {
    pub kind: String,
    pub source_ref: String,
    pub scope_kind: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    pub agent_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_path: Option<String>,
    pub title: String,
    pub snippet: String,
    pub score: f64,
    pub updated_at: DateTime<Utc>,
    pub metadata: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct MemoryGetResult {
    pub kind: String,
    pub source_ref: String,
    pub scope_kind: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    pub agent_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_path: Option<String>,
    pub title: String,
    pub content: String,
    pub truncated: bool,
    pub updated_at: DateTime<Utc>,
    pub metadata: Value,
}

#[derive(Debug, Clone)]
struct MemoryDocument {
    source_ref: String,
    source_kind: String,
    scope_kind: String,
    workspace_id: Option<String>,
    agent_id: String,
    source_path: Option<PathBuf>,
    title: String,
    body: String,
    sanitized_excerpt: String,
    metadata: Value,
    updated_at: DateTime<Utc>,
}

pub fn memory_index_path(storage: &AppStorage) -> PathBuf {
    storage.indexes_dir().join(INDEX_FILENAME)
}

pub fn rebuild_memory_index(storage: &AppStorage, active_workspace_id: Option<&str>) -> Result<()> {
    let mut index = MemoryIndex::open(storage)?;
    index.rebuild(storage, active_workspace_id)
}

pub fn repair_memory_index_for_paths(storage: &AppStorage, changed_paths: &[String]) -> Result<()> {
    let known = known_memory_markdown_sources(storage);
    if !changed_paths.iter().any(|path| {
        let path = Path::new(path);
        known.iter().any(|known| {
            path == known.path || known.path.ends_with(path) || path.ends_with(&known.path)
        })
    }) {
        return Ok(());
    }
    let index = MemoryIndex::open(storage)?;
    repair_known_markdown_sources(storage, &index)
}

pub fn search_memory(
    storage: &AppStorage,
    query: &str,
    limit: usize,
    active_workspace_id: Option<&str>,
    include_all_workspaces: bool,
) -> Result<Vec<MemorySearchResult>> {
    let index = ensure_memory_index_current(storage, active_workspace_id)?;
    index.search(query, limit, active_workspace_id, include_all_workspaces)
}

pub fn get_memory(
    storage: &AppStorage,
    source_ref: &str,
    max_chars: Option<usize>,
    active_workspace_id: Option<&str>,
) -> Result<Option<MemoryGetResult>> {
    let index = ensure_memory_index_current(storage, active_workspace_id)?;
    index.get(source_ref, max_chars, active_workspace_id)
}

fn ensure_memory_index_current(
    storage: &AppStorage,
    active_workspace_id: Option<&str>,
) -> Result<MemoryIndex> {
    let index_file_exists = memory_index_path(storage).exists();
    let mut index = MemoryIndex::open(storage)?;
    if !index_file_exists || memory_index_is_dirty(storage) || !index.has_any_documents()? {
        index.rebuild(storage, active_workspace_id)?;
    } else {
        repair_known_markdown_sources(storage, &index)?;
    }
    Ok(index)
}

fn repair_known_markdown_sources(storage: &AppStorage, index: &MemoryIndex) -> Result<()> {
    for source in known_memory_markdown_sources(storage) {
        if source.path.exists() {
            let Some(document) =
                agent_memory_document(storage, source.name, source.title, &source.path)?
            else {
                continue;
            };
            if index.document_hash(&document.source_ref)? != Some(content_hash(&document.body)) {
                index.upsert_document(&document)?;
            }
        } else {
            index.delete_document(source.source_ref)?;
        }
    }
    Ok(())
}

#[derive(Debug, Clone)]
struct KnownMemoryMarkdownSource {
    source_ref: &'static str,
    name: &'static str,
    title: &'static str,
    path: PathBuf,
}

fn known_memory_markdown_sources(storage: &AppStorage) -> Vec<KnownMemoryMarkdownSource> {
    vec![
        KnownMemoryMarkdownSource {
            source_ref: "agent_memory:self",
            name: "self",
            title: "Agent self memory",
            path: agent_memory_self_path(storage.data_dir()),
        },
        KnownMemoryMarkdownSource {
            source_ref: "agent_memory:operator",
            name: "operator",
            title: "Operator collaboration memory",
            path: agent_memory_operator_path(storage.data_dir()),
        },
    ]
}

fn memory_index_is_dirty(storage: &AppStorage) -> bool {
    storage.indexes_dir().join(DIRTY_FILENAME).exists()
}

fn clear_memory_index_dirty(storage: &AppStorage) -> Result<()> {
    let path = storage.indexes_dir().join(DIRTY_FILENAME);
    if path.exists() {
        fs::remove_file(path)?;
    }
    Ok(())
}

struct MemoryIndex {
    connection: Connection,
}

impl MemoryIndex {
    fn open(storage: &AppStorage) -> Result<Self> {
        fs::create_dir_all(storage.indexes_dir())
            .with_context(|| format!("failed to create {}", storage.indexes_dir().display()))?;
        let connection = Connection::open(memory_index_path(storage))?;
        let index = Self { connection };
        index.ensure_schema()?;
        Ok(index)
    }

    fn ensure_schema(&self) -> Result<()> {
        if self.table_exists("memory_documents")?
            && !self.table_has_column("memory_documents", "original_body")?
        {
            self.connection.execute_batch(
                r#"
                DROP TABLE IF EXISTS memory_documents_fts;
                DROP TABLE IF EXISTS memory_documents;
                "#,
            )?;
        }
        self.connection.execute_batch(
            r#"
            CREATE TABLE IF NOT EXISTS memory_documents (
                source_ref TEXT PRIMARY KEY,
                source_kind TEXT NOT NULL,
                scope_kind TEXT NOT NULL,
                workspace_id TEXT,
                agent_id TEXT NOT NULL,
                source_path TEXT,
                title TEXT NOT NULL,
                original_body TEXT NOT NULL,
                body TEXT NOT NULL,
                sanitized_excerpt TEXT NOT NULL,
                metadata_json TEXT NOT NULL,
                content_hash TEXT NOT NULL,
                updated_at TEXT NOT NULL
            );
            CREATE VIRTUAL TABLE IF NOT EXISTS memory_documents_fts
            USING fts5(source_ref UNINDEXED, title, body, sanitized_excerpt, tokenize='unicode61');
            "#,
        )?;
        Ok(())
    }

    fn table_exists(&self, table: &str) -> Result<bool> {
        self.connection
            .query_row(
                "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1 LIMIT 1",
                [table],
                |_| Ok(true),
            )
            .optional()
            .map(|value| value.unwrap_or(false))
            .map_err(Into::into)
    }

    fn table_has_column(&self, table: &str, column: &str) -> Result<bool> {
        let mut statement = self
            .connection
            .prepare(&format!("PRAGMA table_info({table})"))?;
        let rows = statement.query_map([], |row| row.get::<_, String>(1))?;
        for row in rows {
            if row? == column {
                return Ok(true);
            }
        }
        Ok(false)
    }

    fn rebuild(&mut self, storage: &AppStorage, active_workspace_id: Option<&str>) -> Result<()> {
        let transaction = self.connection.transaction()?;
        transaction.execute("DELETE FROM memory_documents", [])?;
        transaction.execute("DELETE FROM memory_documents_fts", [])?;
        for document in collect_documents(storage, active_workspace_id)? {
            upsert_document_tx(&transaction, &document)?;
        }
        transaction.commit()?;
        clear_memory_index_dirty(storage)?;
        Ok(())
    }

    fn upsert_document(&self, document: &MemoryDocument) -> Result<()> {
        upsert_document_tx(&self.connection, document)
    }

    fn delete_document(&self, source_ref: &str) -> Result<()> {
        self.connection.execute(
            "DELETE FROM memory_documents_fts WHERE source_ref = ?1",
            [source_ref],
        )?;
        self.connection.execute(
            "DELETE FROM memory_documents WHERE source_ref = ?1",
            [source_ref],
        )?;
        Ok(())
    }

    fn has_any_documents(&self) -> Result<bool> {
        let count: i64 =
            self.connection
                .query_row("SELECT COUNT(*) FROM memory_documents", [], |row| {
                    row.get(0)
                })?;
        Ok(count > 0)
    }

    fn document_hash(&self, source_ref: &str) -> Result<Option<String>> {
        self.connection
            .query_row(
                "SELECT content_hash FROM memory_documents WHERE source_ref = ?1",
                [source_ref],
                |row| row.get(0),
            )
            .optional()
            .map_err(Into::into)
    }

    fn search(
        &self,
        query: &str,
        limit: usize,
        active_workspace_id: Option<&str>,
        include_all_workspaces: bool,
    ) -> Result<Vec<MemorySearchResult>> {
        let query = search_query(query);
        let limit = limit.clamp(1, SEARCH_LIMIT_MAX);
        let workspace_filter = if include_all_workspaces {
            None
        } else {
            active_workspace_id.map(ToString::to_string)
        };
        let include_all_workspaces = include_all_workspaces as i64;
        let mut statement = self.connection.prepare(
            r#"
            SELECT d.source_ref, d.source_kind, d.scope_kind, d.workspace_id, d.agent_id,
                   d.source_path, d.title, d.sanitized_excerpt, d.metadata_json,
                   d.updated_at, bm25(memory_documents_fts) AS score
            FROM memory_documents_fts
            JOIN memory_documents d ON d.source_ref = memory_documents_fts.source_ref
            WHERE memory_documents_fts MATCH ?1
              AND (?3 OR d.scope_kind = 'agent' OR (?2 IS NOT NULL AND d.workspace_id = ?2))
            ORDER BY score ASC, d.updated_at DESC
            LIMIT ?4
            "#,
        )?;
        let rows = statement.query_map(
            params![
                query,
                workspace_filter,
                include_all_workspaces,
                limit as i64
            ],
            |row| {
                let metadata_json: String = row.get(8)?;
                let updated_at: String = row.get(9)?;
                Ok(MemorySearchResult {
                    kind: row.get(1)?,
                    source_ref: row.get(0)?,
                    scope_kind: row.get(2)?,
                    workspace_id: row.get(3)?,
                    agent_id: row.get(4)?,
                    source_path: row.get(5)?,
                    title: row.get(6)?,
                    snippet: row.get(7)?,
                    score: row.get(10)?,
                    updated_at: DateTime::parse_from_rfc3339(&updated_at)
                        .map(|dt| dt.with_timezone(&Utc))
                        .unwrap_or_else(|_| Utc::now()),
                    metadata: serde_json::from_str(&metadata_json).unwrap_or(Value::Null),
                })
            },
        )?;
        let mut results = Vec::new();
        for row in rows {
            results.push(row?);
        }
        Ok(results)
    }

    fn get(
        &self,
        source_ref: &str,
        max_chars: Option<usize>,
        active_workspace_id: Option<&str>,
    ) -> Result<Option<MemoryGetResult>> {
        let max_chars = max_chars
            .unwrap_or(GET_CHARS_DEFAULT)
            .clamp(1, GET_CHARS_MAX);
        let workspace_filter = active_workspace_id.map(ToString::to_string);
        self.connection
            .query_row(
                r#"
                SELECT source_ref, source_kind, scope_kind, workspace_id, agent_id, source_path,
                       title, original_body, metadata_json, updated_at
                FROM memory_documents
                WHERE source_ref = ?1
                  AND (scope_kind = 'agent' OR (?2 IS NOT NULL AND workspace_id = ?2))
                "#,
                params![source_ref, workspace_filter],
                |row| {
                    let content: String = row.get(7)?;
                    let metadata_json: String = row.get(8)?;
                    let updated_at: String = row.get(9)?;
                    let (content, truncated) = truncate_chars(&content, max_chars);
                    Ok(MemoryGetResult {
                        source_ref: row.get(0)?,
                        kind: row.get(1)?,
                        scope_kind: row.get(2)?,
                        workspace_id: row.get(3)?,
                        agent_id: row.get(4)?,
                        source_path: row.get(5)?,
                        title: row.get(6)?,
                        content,
                        truncated,
                        updated_at: DateTime::parse_from_rfc3339(&updated_at)
                            .map(|dt| dt.with_timezone(&Utc))
                            .unwrap_or_else(|_| Utc::now()),
                        metadata: serde_json::from_str(&metadata_json).unwrap_or(Value::Null),
                    })
                },
            )
            .optional()
            .map_err(Into::into)
    }
}

fn upsert_document_tx(connection: &Connection, document: &MemoryDocument) -> Result<()> {
    let metadata_json = serde_json::to_string(&document.metadata)?;
    let hash = content_hash(&document.body);
    let source_path = document
        .source_path
        .as_ref()
        .map(|path| path.display().to_string());
    connection.execute(
        r#"
        INSERT INTO memory_documents (
            source_ref, source_kind, scope_kind, workspace_id, agent_id, source_path,
            title, original_body, body, sanitized_excerpt, metadata_json, content_hash, updated_at
        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
        ON CONFLICT(source_ref) DO UPDATE SET
            source_kind=excluded.source_kind,
            scope_kind=excluded.scope_kind,
            workspace_id=excluded.workspace_id,
            agent_id=excluded.agent_id,
            source_path=excluded.source_path,
            title=excluded.title,
            original_body=excluded.original_body,
            body=excluded.body,
            sanitized_excerpt=excluded.sanitized_excerpt,
            metadata_json=excluded.metadata_json,
            content_hash=excluded.content_hash,
            updated_at=excluded.updated_at
        "#,
        params![
            document.source_ref,
            document.source_kind,
            document.scope_kind,
            document.workspace_id,
            document.agent_id,
            source_path,
            document.title,
            document.body,
            indexed_text(&document.body),
            document.sanitized_excerpt,
            metadata_json,
            hash,
            document.updated_at.to_rfc3339(),
        ],
    )?;
    connection.execute(
        "DELETE FROM memory_documents_fts WHERE source_ref = ?1",
        [document.source_ref.as_str()],
    )?;
    connection.execute(
        "INSERT INTO memory_documents_fts(source_ref, title, body, sanitized_excerpt) VALUES (?1, ?2, ?3, ?4)",
        params![
            document.source_ref,
            indexed_text(&document.title),
            indexed_text(&document.body),
            indexed_text(&document.sanitized_excerpt)
        ],
    )?;
    Ok(())
}

fn collect_documents(
    storage: &AppStorage,
    _active_workspace_id: Option<&str>,
) -> Result<Vec<MemoryDocument>> {
    let mut documents = Vec::new();
    documents.extend(agent_memory_documents(storage)?);
    documents.extend(workspace_profile_documents(storage)?);
    documents.extend(brief_documents(storage)?);
    documents.extend(context_episode_documents(storage)?);
    documents.extend(work_item_documents(storage)?);
    documents.extend(task_documents(storage)?);
    documents.extend(command_execution_documents(storage)?);
    Ok(documents)
}

fn agent_memory_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    let mut documents = Vec::new();
    for source in known_memory_markdown_sources(storage) {
        if let Some(document) =
            agent_memory_document(storage, source.name, source.title, &source.path)?
        {
            documents.push(document);
        }
    }
    Ok(documents)
}

fn agent_memory_document(
    storage: &AppStorage,
    name: &str,
    title: &str,
    path: &Path,
) -> Result<Option<MemoryDocument>> {
    if !path.exists() {
        return Ok(None);
    }
    let body =
        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
    Ok(Some(MemoryDocument {
        source_ref: format!("agent_memory:{name}"),
        source_kind: "agent_memory_markdown".into(),
        scope_kind: "agent".into(),
        workspace_id: None,
        agent_id: storage_agent_id(storage),
        source_path: Some(path.to_path_buf()),
        title: title.into(),
        sanitized_excerpt: excerpt(&body),
        body,
        metadata: json!({
            "memory_name": name,
            "governance_surface": "curated_durable_memory",
            "provenance_class": "agent_home_memory_markdown",
            "trust_class": "agent_curated",
        }),
        updated_at: file_updated_at(path),
    }))
}

fn workspace_profile_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    let mut latest = BTreeMap::<String, WorkspaceEntry>::new();
    for entry in storage.read_recent_workspace_entries(usize::MAX)? {
        latest.insert(entry.workspace_id.clone(), entry);
    }
    Ok(latest
        .into_values()
        .map(|entry| {
            let title = entry
                .repo_name
                .clone()
                .unwrap_or_else(|| format!("Workspace {}", entry.workspace_id));
            let body = format!(
                "{}\nworkspace_id: {}\nworkspace_anchor: {}",
                title,
                entry.workspace_id,
                entry.workspace_anchor.display()
            );
            MemoryDocument {
                source_ref: format!("workspace_profile:{}", entry.workspace_id),
                source_kind: "workspace_profile".into(),
                scope_kind: "workspace".into(),
                workspace_id: Some(entry.workspace_id.clone()),
                agent_id: storage_agent_id(storage),
                source_path: Some(entry.workspace_anchor.clone()),
                title,
                sanitized_excerpt: excerpt(&body),
                body,
                metadata: json!({
                    "workspace_anchor": entry.workspace_anchor,
                    "governance_surface": "workspace_profile_projection",
                    "provenance_class": "workspace_registry",
                    "trust_class": "runtime_projection",
                }),
                updated_at: entry.updated_at,
            }
        })
        .collect())
}

fn brief_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    Ok(storage
        .read_recent_briefs(REBUILD_BRIEF_LIMIT)?
        .into_iter()
        .filter(|brief| !brief.text.trim().is_empty())
        .map(|brief| brief_document(storage, brief))
        .collect())
}

fn brief_document(storage: &AppStorage, brief: BriefRecord) -> MemoryDocument {
    MemoryDocument {
        source_ref: format!("brief:{}", brief.id),
        source_kind: "brief".into(),
        scope_kind: "workspace".into(),
        workspace_id: Some(brief.workspace_id),
        agent_id: brief.agent_id,
        source_path: None,
        title: format!("Brief {:?}", brief.kind),
        sanitized_excerpt: excerpt(&brief.text),
        body: brief.text,
        metadata: json!({
            "work_item_id": brief.work_item_id,
            "related_message_id": brief.related_message_id,
            "related_task_id": brief.related_task_id,
            "agent_home": storage.data_dir(),
            "governance_surface": "runtime_evidence",
            "provenance_class": "brief_record",
            "trust_class": "runtime_evidence",
        }),
        updated_at: brief.created_at,
    }
}

fn context_episode_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    Ok(storage
        .read_recent_context_episodes(REBUILD_EPISODE_LIMIT)?
        .into_iter()
        .filter(|episode| !episode.summary.trim().is_empty())
        .map(episode_document)
        .collect())
}

fn episode_document(episode: ContextEpisodeRecord) -> MemoryDocument {
    let body = [
        episode.summary.clone(),
        episode.work_summary.clone().unwrap_or_default(),
        episode.commands.join("\n"),
        episode.verification.join("\n"),
        episode.decisions.join("\n"),
        episode.carry_forward.join("\n"),
    ]
    .into_iter()
    .filter(|part| !part.trim().is_empty())
    .collect::<Vec<_>>()
    .join("\n");
    MemoryDocument {
        source_ref: format!("episode:{}", episode.id),
        source_kind: "context_episode".into(),
        scope_kind: "workspace".into(),
        workspace_id: Some(episode.workspace_id),
        agent_id: episode.agent_id,
        source_path: None,
        title: episode
            .work_summary
            .clone()
            .unwrap_or_else(|| "Context episode".into()),
        sanitized_excerpt: excerpt(&body),
        body,
        metadata: json!({
            "episode_id": episode.id,
            "current_work_item_id": episode.current_work_item_id,
            "boundary_reason": episode.boundary_reason,
            "working_set_files": episode.working_set_files,
            "governance_surface": "runtime_evidence",
            "provenance_class": "context_episode_record",
            "trust_class": "runtime_evidence",
        }),
        updated_at: episode.finalized_at,
    }
}

fn work_item_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    let mut latest = BTreeMap::<String, WorkItemRecord>::new();
    for item in storage.read_recent_work_items(REBUILD_WORK_ITEM_LIMIT)? {
        latest.insert(item.id.clone(), item);
    }
    Ok(latest.into_values().map(work_item_document).collect())
}

fn work_item_document(item: WorkItemRecord) -> MemoryDocument {
    let body = work_item_document_body(&item);
    MemoryDocument {
        source_ref: format!("work_item:{}", item.id),
        source_kind: "work_item".into(),
        scope_kind: "workspace".into(),
        workspace_id: Some(item.workspace_id),
        agent_id: item.agent_id,
        source_path: None,
        title: item.objective.clone(),
        sanitized_excerpt: excerpt(&body),
        body,
        metadata: json!({
            "work_item_id": item.id,
            "state": item.state,
            "blocked_by": item.blocked_by,
            "governance_surface": "runtime_evidence",
            "provenance_class": "work_item_record",
            "trust_class": "runtime_evidence",
        }),
        updated_at: item.updated_at,
    }
}

fn work_item_document_body(item: &WorkItemRecord) -> String {
    let mut lines = vec![
        format!("Objective: {}", item.objective),
        format!(
            "Plan status: {}",
            work_item_plan_status_label(item.plan_status)
        ),
    ];
    if let Some(plan) = item.plan.as_deref().filter(|plan| !plan.trim().is_empty()) {
        lines.push("Plan:".into());
        lines.push(plan.to_string());
    }
    if !item.todo_list.is_empty() {
        lines.push("Todo list:".into());
        for todo in &item.todo_list {
            lines.push(format!(
                "- [{}] {}",
                todo_item_state_label(todo.state),
                todo.text
            ));
        }
    }
    lines.join("\n")
}

fn task_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    Ok(storage
        .latest_task_records_from_recent(REBUILD_TASK_HISTORY_LIMIT)?
        .into_iter()
        .map(task_document)
        .collect())
}

fn task_document(task: TaskRecord) -> MemoryDocument {
    let agent_id = task.agent_id.clone();
    let task_kind = task.kind.as_str();
    let command = CommandTaskStatusSnapshot::identity_from_task_record(&task);
    let command = command.as_ref();
    let command_text = task
        .detail
        .as_ref()
        .and_then(|detail| detail.get("cmd"))
        .and_then(Value::as_str)
        .or_else(|| command.and_then(|entry| entry.cmd.as_deref()));
    let child_agent_id = task
        .detail
        .as_ref()
        .and_then(|detail| detail.get("child_agent_id"))
        .and_then(Value::as_str);
    let exit_status = task
        .detail
        .as_ref()
        .and_then(|detail| detail.get("exit_status"))
        .and_then(Value::as_i64)
        .and_then(|value| i32::try_from(value).ok());
    let mut body = task_document_body(
        &task,
        task_kind,
        command,
        command_text,
        child_agent_id,
        exit_status,
    );
    if let Some(cmd) = command_text {
        body.push('\n');
        if cmd.contains('\n') {
            body.push_str("cmd:\n");
        } else {
            body.push_str("cmd: ");
        }
        body.push_str(cmd);
    }
    MemoryDocument {
        source_ref: format!("task:{}", task.id),
        source_kind: "task".into(),
        scope_kind: "agent".into(),
        workspace_id: None,
        agent_id: agent_id.clone(),
        source_path: None,
        title: task
            .summary
            .clone()
            .unwrap_or_else(|| format!("Task {}", task.id)),
        sanitized_excerpt: excerpt(&body),
        body,
        metadata: json!({
            "task_id": task.id,
            "kind": task_kind,
            "status": task_status_label(&task.status),
            "summary": task.summary,
            "work_item_id": task.work_item_id,
            "cmd_digest": task_command_digest(command, command_text),
            "cmd_preview": command
                .and_then(|entry| entry.cmd.as_deref())
                .or(command_text)
                .map(command_preview),
            "exit_status": exit_status,
            "agent_id": agent_id,
            "created_at": task.created_at.to_rfc3339(),
            "governance_surface": "runtime_evidence",
            "provenance_class": "task_record",
            "trust_class": "runtime_evidence",
        }),
        updated_at: task.updated_at,
    }
}

fn task_document_body(
    task: &TaskRecord,
    task_kind: &str,
    command: Option<&CommandTaskStatusSnapshot>,
    command_text: Option<&str>,
    child_agent_id: Option<&str>,
    exit_status: Option<i32>,
) -> String {
    let mut lines = vec![
        format!("task_id: {}", task.id),
        format!("kind: {task_kind}"),
        format!("status: {}", task_status_label(&task.status)),
        format!("summary: {}", task.summary.clone().unwrap_or_default()),
        format!("created_at: {}", task.created_at.to_rfc3339()),
        format!("updated_at: {}", task.updated_at.to_rfc3339()),
    ];
    if let Some(work_item_id) = task.work_item_id.as_deref() {
        lines.push(format!("work_item_id: {work_item_id}"));
    }
    if let Some(cmd_digest) = task_command_digest(command, command_text) {
        lines.push(format!("cmd_digest: {cmd_digest}"));
    }
    if let Some(cmd_preview) = command
        .and_then(|entry| entry.cmd.as_deref())
        .or(command_text)
        .map(command_preview)
    {
        lines.push(format!("cmd_preview: {cmd_preview}"));
    }
    if let Some(exit_status) = exit_status {
        lines.push(format!("exit_status: {exit_status}"));
    }
    if let Some(child_agent_id) = child_agent_id {
        lines.push(format!("child_agent_id: {child_agent_id}"));
    }
    lines.join("\n")
}

fn task_command_digest(
    command: Option<&CommandTaskStatusSnapshot>,
    command_text: Option<&str>,
) -> Option<String> {
    command
        .and_then(|entry| {
            entry
                .cmd_digest
                .clone()
                .or_else(|| entry.cmd.as_ref().map(|cmd| command_digest(cmd)))
        })
        .or_else(|| command_text.map(command_digest))
}

fn task_status_label(status: &TaskStatus) -> &'static str {
    match status {
        TaskStatus::Queued => "queued",
        TaskStatus::Running => "running",
        TaskStatus::Cancelling => "cancelling",
        TaskStatus::Completed => "completed",
        TaskStatus::Failed => "failed",
        TaskStatus::Cancelled => "cancelled",
        TaskStatus::Interrupted => "interrupted",
    }
}

fn command_execution_documents(storage: &AppStorage) -> Result<Vec<MemoryDocument>> {
    let mut documents = Vec::new();
    for record in storage.read_recent_tool_executions(REBUILD_COMMAND_RECEIPT_LIMIT)? {
        match record.tool_name.as_str() {
            "ExecCommand" => {
                if let Some(cmd) = record.input.get("cmd").and_then(Value::as_str) {
                    documents.push(command_receipt_document(&record, None, None, cmd));
                }
            }
            "ExecCommandBatch" => {
                if let Some(items) = record.input.get("items").and_then(Value::as_array) {
                    for (offset, item) in items.iter().enumerate() {
                        if let Some(cmd) = item.get("cmd").and_then(Value::as_str) {
                            documents.push(command_receipt_document(
                                &record,
                                Some(offset + 1),
                                Some(item),
                                cmd,
                            ));
                        }
                    }
                }
            }
            _ => {}
        }
    }
    Ok(documents)
}

fn command_receipt_document(
    record: &ToolExecutionRecord,
    batch_item_index: Option<usize>,
    batch_item_input: Option<&Value>,
    cmd: &str,
) -> MemoryDocument {
    let cmd_digest = command_digest(cmd);
    let source_ref = command_receipt_source_ref(&record.id, batch_item_index);
    let title = match batch_item_index {
        Some(index) => format!("{} command receipt item {}", record.tool_name, index),
        None => format!("{} command receipt", record.tool_name),
    };
    let preview = command_preview(cmd);
    let input_json =
        serde_json::to_string_pretty(&record.input).unwrap_or_else(|_| record.input.to_string());
    let batch_item_input = batch_item_input
        .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()));
    let body = format!(
        "tool_execution_id: {}\ntool_name: {}\ncmd_digest: {}\ninput_json:\n{}\n{}cmd:\n{}",
        record.id,
        record.tool_name,
        cmd_digest,
        input_json,
        batch_item_input
            .as_deref()
            .map(|value| format!("batch_item_input_json:\n{value}\n"))
            .unwrap_or_default(),
        cmd
    );
    MemoryDocument {
        source_ref,
        source_kind: "tool_command_receipt".into(),
        scope_kind: "agent".into(),
        workspace_id: None,
        agent_id: record.agent_id.clone(),
        source_path: None,
        title,
        sanitized_excerpt: format!(
            "tool_execution_id={} tool_name={} cmd_digest={} cmd_preview={}",
            record.id, record.tool_name, cmd_digest, preview
        ),
        body,
        metadata: json!({
            "tool_execution_id": record.id,
            "tool_name": record.tool_name,
            "turn_index": record.turn_index,
            "work_item_id": record.work_item_id,
            "batch_item_index": batch_item_index,
            "cmd_digest": cmd_digest,
            "cmd_preview": preview,
            "governance_surface": "runtime_evidence",
            "provenance_class": "tool_execution_record",
            "trust_class": "runtime_evidence",
        }),
        updated_at: record.completed_at.unwrap_or(record.created_at),
    }
}

fn work_item_plan_status_label(status: crate::types::WorkItemPlanStatus) -> &'static str {
    match status {
        crate::types::WorkItemPlanStatus::Draft => "draft",
        crate::types::WorkItemPlanStatus::Ready => "ready",
        crate::types::WorkItemPlanStatus::NeedsInput => "needs_input",
    }
}

fn todo_item_state_label(state: crate::types::TodoItemState) -> &'static str {
    match state {
        crate::types::TodoItemState::Pending => "pending",
        crate::types::TodoItemState::InProgress => "in_progress",
        crate::types::TodoItemState::Completed => "completed",
    }
}

fn storage_agent_id(storage: &AppStorage) -> String {
    storage
        .read_agent()
        .ok()
        .flatten()
        .map(|agent| agent.id)
        .unwrap_or_else(|| "unknown".into())
}

fn file_updated_at(path: &Path) -> DateTime<Utc> {
    fs::metadata(path)
        .and_then(|metadata| metadata.modified())
        .map(DateTime::<Utc>::from)
        .unwrap_or_else(|_| Utc::now())
}

fn content_hash(content: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(content.as_bytes());
    format!("{:x}", hasher.finalize())
}

fn excerpt(text: &str) -> String {
    text.split_whitespace()
        .take(48)
        .collect::<Vec<_>>()
        .join(" ")
}

fn indexed_text(text: &str) -> String {
    let mut expanded = String::with_capacity(text.len() * 2);
    expanded.push_str(text);
    expanded.push('\n');
    expanded.push_str(&mixed_cjk_bigrams(text));
    expanded
}

fn search_query(query: &str) -> String {
    let expanded = indexed_text(query);
    expanded
        .split_whitespace()
        .filter(|term| !term.is_empty())
        .map(escape_fts_term)
        .collect::<Vec<_>>()
        .join(" OR ")
}

fn escape_fts_term(term: &str) -> String {
    format!("\"{}\"", term.replace('"', "\"\""))
}

fn mixed_cjk_bigrams(text: &str) -> String {
    let chars = text.chars().collect::<Vec<_>>();
    let mut grams = Vec::new();
    for window in chars.windows(2) {
        if window.iter().all(|ch| is_cjk(*ch)) {
            grams.push(window.iter().collect::<String>());
        }
    }
    grams.join(" ")
}

fn is_cjk(ch: char) -> bool {
    matches!(
        ch as u32,
        0x3400..=0x4DBF
            | 0x4E00..=0x9FFF
            | 0xF900..=0xFAFF
            | 0x20000..=0x2A6DF
            | 0x2A700..=0x2B73F
            | 0x2B740..=0x2B81F
            | 0x2B820..=0x2CEAF
    )
}

fn truncate_chars(text: &str, max_chars: usize) -> (String, bool) {
    let mut chars = text.chars();
    let truncated = chars.clone().nth(max_chars).is_some();
    let content = chars.by_ref().take(max_chars).collect::<String>();
    (content, truncated)
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;
    use crate::{
        agent_template::ensure_agent_home_layout,
        types::{
            AgentState, BriefKind, ContextEpisodeRecord, EpisodeBoundaryReason, TaskKind,
            TaskRecord, TaskStatus, TodoItem, TodoItemState, WorkItemPlanStatus, WorkItemState,
        },
    };
    use serde_json::json;

    fn brief_with_workspace(
        agent_id: &str,
        kind: BriefKind,
        text: &str,
        workspace_id: &str,
    ) -> BriefRecord {
        let mut brief = BriefRecord::new(agent_id, kind, text, None, None);
        brief.workspace_id = workspace_id.to_string();
        brief
    }

    fn work_item_with_workspace(
        agent_id: &str,
        objective: &str,
        status: WorkItemState,
        workspace_id: &str,
    ) -> WorkItemRecord {
        let mut work_item = WorkItemRecord::new(agent_id, objective, status);
        work_item.workspace_id = workspace_id.to_string();
        work_item
    }

    fn task_record(
        id: &str,
        agent_id: &str,
        kind: TaskKind,
        status: TaskStatus,
        summary: &str,
        work_item_id: Option<String>,
        detail: Option<Value>,
        created_offset_seconds: i64,
        updated_offset_seconds: i64,
    ) -> TaskRecord {
        let now = Utc::now();
        TaskRecord {
            id: id.into(),
            agent_id: agent_id.into(),
            kind,
            status,
            created_at: now + chrono::Duration::seconds(created_offset_seconds),
            updated_at: now + chrono::Duration::seconds(updated_offset_seconds),
            parent_message_id: None,
            work_item_id,
            summary: Some(summary.into()),
            detail,
            recovery: None,
        }
    }

    #[test]
    fn memory_search_indexes_agent_memory_and_repairs_external_edits() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        fs::write(
            agent_memory_self_path(dir.path()),
            "The agent prefers precise release checklists.",
        )
        .unwrap();

        rebuild_memory_index(&storage, None).unwrap();
        let results = search_memory(&storage, "release", 10, None, false).unwrap();
        assert_eq!(results[0].kind, "agent_memory_markdown");
        assert_eq!(
            results[0].metadata["governance_surface"].as_str(),
            Some("curated_durable_memory")
        );
        assert_eq!(
            results[0].metadata["trust_class"].as_str(),
            Some("agent_curated")
        );

        fs::write(
            agent_memory_self_path(dir.path()),
            "The agent now remembers 混合 搜索 diagnostics.",
        )
        .unwrap();
        let results = search_memory(&storage, "混合搜索", 10, None, false).unwrap();
        assert!(results.iter().any(|result| {
            result.source_ref == "agent_memory:self" && result.snippet.contains("混合")
        }));
    }

    #[test]
    fn memory_get_returns_exact_markdown_and_runtime_source_content() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        let markdown = "The agent now remembers 混合 搜索 diagnostics.";
        fs::write(agent_memory_self_path(dir.path()), markdown).unwrap();
        let brief = brief_with_workspace(
            "default",
            BriefKind::Result,
            "runtime exact evidence body",
            "ws-holon",
        );
        let brief_ref = format!("brief:{}", brief.id);
        storage.append_brief(&brief).unwrap();
        let other_brief = brief_with_workspace(
            "default",
            BriefKind::Result,
            "other workspace exact evidence",
            "ws-other",
        );
        let other_brief_ref = format!("brief:{}", other_brief.id);
        storage.append_brief(&other_brief).unwrap();

        rebuild_memory_index(&storage, Some("ws-holon")).unwrap();

        let memory = get_memory(&storage, "agent_memory:self", None, Some("ws-holon"))
            .unwrap()
            .unwrap();
        assert_eq!(memory.content, markdown);
        assert!(!memory.content.contains("混合 搜索 diagnostics\n混合"));
        assert!(!memory.truncated);

        let memory = get_memory(&storage, &brief_ref, Some(12), Some("ws-holon"))
            .unwrap()
            .unwrap();
        assert_eq!(memory.content, "runtime exac");
        assert!(memory.truncated);

        let memory = get_memory(&storage, &brief_ref, None, Some("ws-holon"))
            .unwrap()
            .unwrap();
        assert_eq!(memory.content, "runtime exact evidence body");
        assert!(!memory.truncated);

        assert!(
            get_memory(&storage, &other_brief_ref, None, Some("ws-holon"))
                .unwrap()
                .is_none()
        );
        assert!(get_memory(&storage, &brief_ref, None, None)
            .unwrap()
            .is_none());
    }

    #[test]
    fn deleting_known_memory_markdown_removes_index_row() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        let path = agent_memory_self_path(dir.path());
        fs::write(&path, "deletable precise memory").unwrap();
        rebuild_memory_index(&storage, None).unwrap();
        assert!(search_memory(&storage, "deletable", 10, None, false)
            .unwrap()
            .iter()
            .any(|result| result.source_ref == "agent_memory:self"));

        fs::remove_file(&path).unwrap();
        repair_memory_index_for_paths(&storage, &[path.display().to_string()]).unwrap();

        assert!(!search_memory(&storage, "deletable", 10, None, false)
            .unwrap()
            .iter()
            .any(|result| result.source_ref == "agent_memory:self"));
        assert!(get_memory(&storage, "agent_memory:self", None, None)
            .unwrap()
            .is_none());
    }

    #[test]
    fn memory_search_indexes_workspace_profile_briefs_episodes_and_work_items() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        storage
            .append_workspace_entry(&WorkspaceEntry::new(
                "ws-holon",
                PathBuf::from("/repo/holon"),
                Some("holon runtime".into()),
            ))
            .unwrap();
        storage
            .append_workspace_entry(&WorkspaceEntry::new(
                "ws-other",
                PathBuf::from("/repo/other"),
                Some("other runtime".into()),
            ))
            .unwrap();
        storage
            .append_brief(&brief_with_workspace(
                "default",
                BriefKind::Result,
                "cache diagnostics completed for holon",
                "ws-holon",
            ))
            .unwrap();
        storage
            .append_brief(&brief_with_workspace(
                "default",
                BriefKind::Result,
                "other workspace cache diagnostics",
                "ws-other",
            ))
            .unwrap();
        let mut work_item = WorkItemRecord::new(
            "default",
            "MemorySearch index implementation",
            WorkItemState::Completed,
        );
        work_item.workspace_id = "ws-holon".into();
        work_item.plan_status = WorkItemPlanStatus::Ready;
        work_item.plan = Some("Persist checksum-oriented task understanding in recall.".into());
        work_item.todo_list = vec![
            TodoItem {
                text: "Index durable objective plan text".into(),
                state: TodoItemState::Completed,
            },
            TodoItem {
                text: "Verify checklist retrieval marker".into(),
                state: TodoItemState::InProgress,
            },
        ];
        storage.append_work_item(&work_item).unwrap();
        storage
            .append_context_episode(&ContextEpisodeRecord {
                id: "episode-1".into(),
                agent_id: "default".into(),
                workspace_id: "ws-holon".into(),
                created_at: Utc::now(),
                finalized_at: Utc::now(),
                start_turn_index: 1,
                end_turn_index: 2,
                start_message_count: 1,
                end_message_count: 3,
                boundary_reason: EpisodeBoundaryReason::HardTurnCap,
                current_work_item_id: Some(work_item.id.clone()),
                objective: Some("memory search".into()),
                work_summary: Some("index worker".into()),
                scope_hints: vec![],
                summary: "Implemented workspace-aware recall over runtime evidence".into(),
                working_set_files: vec!["src/memory/index.rs".into()],
                commands: vec![],
                verification: vec!["cargo test memory".into()],
                decisions: vec!["Use SQLite FTS5".into()],
                carry_forward: vec![],
                waiting_on: vec![],
            })
            .unwrap();

        rebuild_memory_index(&storage, Some("ws-holon")).unwrap();
        let results = search_memory(&storage, "holon", 10, Some("ws-holon"), false).unwrap();
        assert!(results
            .iter()
            .any(|result| result.kind == "workspace_profile"));
        let brief_result = results
            .iter()
            .find(|result| result.kind == "brief")
            .expect("brief memory result");
        assert_eq!(
            brief_result.metadata["governance_surface"].as_str(),
            Some("runtime_evidence")
        );
        assert_eq!(
            brief_result.metadata["provenance_class"].as_str(),
            Some("brief_record")
        );
        let results = search_memory(&storage, "SQLite", 10, Some("ws-holon"), false).unwrap();
        assert!(results
            .iter()
            .any(|result| result.kind == "context_episode"));
        let results = search_memory(&storage, "MemorySearch", 10, Some("ws-holon"), false).unwrap();
        assert!(results.iter().any(|result| result.kind == "work_item"));
        let results = search_memory(&storage, "checksum", 10, Some("ws-holon"), false).unwrap();
        assert!(results.iter().any(|result| result.kind == "work_item"));
        let results = search_memory(&storage, "checklist", 10, Some("ws-holon"), false).unwrap();
        assert!(results.iter().any(|result| result.kind == "work_item"));
        let work_item_doc = get_memory(
            &storage,
            &format!("work_item:{}", work_item.id),
            None,
            Some("ws-holon"),
        )
        .unwrap()
        .expect("work item memory document");
        assert!(work_item_doc.content.contains("Plan status: ready"));
        assert!(work_item_doc
            .content
            .contains("Verify checklist retrieval marker"));
        assert!(results.iter().all(|result| result.scope_kind == "agent"
            || result.workspace_id.as_deref() == Some("ws-holon")));
        let other_results = search_memory(&storage, "other", 10, Some("ws-holon"), false).unwrap();
        assert!(other_results.iter().all(|result| {
            result.scope_kind == "agent" || result.workspace_id.as_deref() == Some("ws-holon")
        }));
        let unscoped_results = search_memory(&storage, "MemorySearch", 10, None, false).unwrap();
        assert!(unscoped_results
            .iter()
            .all(|result| result.scope_kind == "agent"));

        storage
            .append_brief(&brief_with_workspace(
                "default",
                BriefKind::Result,
                "fresh dirty marker recall",
                "ws-holon",
            ))
            .unwrap();
        let results = search_memory(&storage, "fresh", 10, Some("ws-holon"), false).unwrap();
        assert!(results
            .iter()
            .any(|result| result.kind == "brief" && result.snippet.contains("fresh")));
    }

    #[test]
    fn missing_index_rebuilds_all_existing_memory_sources_before_repair() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        storage
            .append_workspace_entry(&WorkspaceEntry::new(
                "ws-existing",
                PathBuf::from("/repo/existing"),
                Some("existing workspace".into()),
            ))
            .unwrap();
        storage
            .append_brief(&brief_with_workspace(
                "default",
                BriefKind::Result,
                "existing ledger brief memory",
                "ws-existing",
            ))
            .unwrap();
        let work_item = work_item_with_workspace(
            "default",
            "existing work item",
            WorkItemState::Completed,
            "ws-existing",
        );
        storage.append_work_item(&work_item).unwrap();
        storage
            .append_context_episode(&ContextEpisodeRecord {
                id: "episode-existing".into(),
                agent_id: "default".into(),
                workspace_id: "ws-existing".into(),
                created_at: Utc::now(),
                finalized_at: Utc::now(),
                start_turn_index: 1,
                end_turn_index: 2,
                start_message_count: 1,
                end_message_count: 2,
                boundary_reason: EpisodeBoundaryReason::HardTurnCap,
                current_work_item_id: Some(work_item.id),
                objective: Some("existing episode".into()),
                work_summary: Some("existing episode summary".into()),
                scope_hints: vec![],
                summary: "existing context episode memory".into(),
                working_set_files: vec![],
                commands: vec![],
                verification: vec![],
                decisions: vec![],
                carry_forward: vec![],
                waiting_on: vec![],
            })
            .unwrap();

        let _ = fs::remove_file(memory_index_path(&storage));
        let _ = fs::remove_file(storage.indexes_dir().join(DIRTY_FILENAME));

        let results = search_memory(&storage, "existing", 10, Some("ws-existing"), false).unwrap();
        assert!(results.iter().any(|result| result.kind == "brief"));
        assert!(results.iter().any(|result| result.kind == "work_item"));
        assert!(results
            .iter()
            .any(|result| result.kind == "context_episode"));
    }

    #[test]
    fn controlled_changed_paths_repair_known_memory_markdown_only() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        fs::write(
            agent_memory_operator_path(dir.path()),
            "operator prefers short status",
        )
        .unwrap();
        rebuild_memory_index(&storage, None).unwrap();

        fs::write(
            agent_memory_operator_path(dir.path()),
            "operator prefers direct Chinese status",
        )
        .unwrap();
        repair_memory_index_for_paths(
            &storage,
            &[agent_memory_operator_path(dir.path()).display().to_string()],
        )
        .unwrap();
        let results = search_memory(&storage, "Chinese", 10, None, false).unwrap();
        assert!(results
            .iter()
            .any(|result| result.source_ref == "agent_memory:operator"));
    }

    #[test]
    fn ordinary_workspace_markdown_is_not_indexed_as_memory() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();
        let workspace = dir.path().join("workspace");
        fs::create_dir_all(&workspace).unwrap();
        let readme_marker = "governance_sentinel_924_workspace_markdown";
        fs::write(
            workspace.join("README.md"),
            format!("ordinary workspace markdown {readme_marker} should not become Holon memory"),
        )
        .unwrap();
        storage
            .append_workspace_entry(&WorkspaceEntry::new(
                "ws-markdown",
                workspace,
                Some("markdown workspace".into()),
            ))
            .unwrap();

        rebuild_memory_index(&storage, Some("ws-markdown")).unwrap();
        let results =
            search_memory(&storage, readme_marker, 10, Some("ws-markdown"), false).unwrap();

        assert!(
            results.is_empty(),
            "workspace README marker must not be searchable as Holon memory: {results:?}"
        );
    }

    #[test]
    fn memory_search_indexes_task_records_with_summary_and_work_item_lookup() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();

        storage
            .append_task(&task_record(
                "task-lookup-1246",
                "default",
                TaskKind::CommandTask,
                TaskStatus::Completed,
                "lookup task command summary",
                Some("wi-lookup-1246".into()),
                Some(json!({"cmd": "echo task lookup evidence"})),
                0,
                0,
            ))
            .unwrap();
        storage
            .append_task(&task_record(
                "task-work-item-1246",
                "default",
                TaskKind::ChildAgentTask,
                TaskStatus::Completed,
                "work item specific task summary",
                None,
                None,
                1,
                1,
            ))
            .unwrap();

        rebuild_memory_index(&storage, None).unwrap();

        let task_id_results = search_memory(&storage, "task-lookup-1246", 10, None, false).unwrap();
        assert!(task_id_results
            .iter()
            .any(|result| result.source_ref == "task:task-lookup-1246"));

        let summary_results =
            search_memory(&storage, "lookup task command summary", 10, None, false).unwrap();
        assert!(summary_results
            .iter()
            .any(|result| result.source_ref == "task:task-lookup-1246"));
        let work_item_lookup_results =
            search_memory(&storage, "work item specific task summary", 10, None, false).unwrap();
        assert!(work_item_lookup_results
            .iter()
            .any(|result| result.source_ref == "task:task-work-item-1246"));
        let work_item_results = search_memory(&storage, "wi-lookup-1246", 10, None, false).unwrap();
        assert!(work_item_results
            .iter()
            .any(|result| result.metadata["work_item_id"].as_str() == Some("wi-lookup-1246")));
        assert!(work_item_results
            .iter()
            .any(|result| result.source_ref == "task:task-lookup-1246"));
    }

    #[test]
    fn memory_search_indexes_task_records_by_command_fragment_and_digest() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();

        let command = "rg -n \"memory\" src/memory/index.rs && echo task_digest_1246";
        let digest = command_digest(command);

        storage
            .append_task(&task_record(
                "task-command-1246",
                "default",
                TaskKind::CommandTask,
                TaskStatus::Completed,
                "command task command digest check",
                None,
                Some(json!({"cmd": command})),
                0,
                0,
            ))
            .unwrap();

        rebuild_memory_index(&storage, None).unwrap();

        let fragment_results =
            search_memory(&storage, "task_digest_1246", 10, None, false).unwrap();
        assert!(fragment_results
            .iter()
            .any(|result| result.source_ref == "task:task-command-1246"));
        let digest_results = search_memory(&storage, &digest, 10, None, false).unwrap();
        assert!(digest_results
            .iter()
            .any(|result| result.source_ref == "task:task-command-1246"));
        assert!(digest_results
            .iter()
            .any(|result| result.metadata["cmd_digest"].as_str() == Some(digest.as_str())));
        assert!(digest_results.iter().all(|result| result.kind == "task"));
    }

    #[test]
    fn memory_search_task_index_uses_latest_snapshot_only() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();

        storage
            .append_task(&task_record(
                "task-repeat-1246",
                "default",
                TaskKind::CommandTask,
                TaskStatus::Running,
                "repeat task running",
                None,
                Some(json!({"cmd": "echo task repeat"})),
                0,
                0,
            ))
            .unwrap();
        storage
            .append_task(&task_record(
                "task-repeat-1246",
                "default",
                TaskKind::CommandTask,
                TaskStatus::Completed,
                "repeat task completed",
                None,
                Some(json!({"cmd": "echo task repeat"})),
                1,
                1,
            ))
            .unwrap();

        rebuild_memory_index(&storage, None).unwrap();
        let results = search_memory(&storage, "repeat task", 10, None, false).unwrap();
        let task_results: Vec<_> = results
            .iter()
            .filter(|result| result.source_ref == "task:task-repeat-1246")
            .collect();
        assert_eq!(task_results.len(), 1);
        assert_eq!(
            task_results[0].metadata["status"].as_str(),
            Some("completed")
        );
        assert_eq!(
            task_results[0].metadata["summary"].as_str(),
            Some("repeat task completed")
        );
    }

    #[test]
    fn memory_search_task_index_uses_bounded_recent_task_history() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();

        storage
            .append_task(&task_record(
                "task-outside-bound-1270",
                "default",
                TaskKind::CommandTask,
                TaskStatus::Completed,
                "outside bounded history sentinel 1270",
                None,
                Some(json!({"cmd": "echo outside_bound_1270"})),
                0,
                0,
            ))
            .unwrap();
        for index in 0..REBUILD_TASK_HISTORY_LIMIT {
            storage
                .append_task(&task_record(
                    &format!("task-recent-bound-{index}"),
                    "default",
                    TaskKind::CommandTask,
                    TaskStatus::Completed,
                    "recent bounded history task",
                    None,
                    Some(json!({"cmd": format!("echo recent_bound_{index}")})),
                    index as i64 + 1,
                    index as i64 + 1,
                ))
                .unwrap();
        }

        rebuild_memory_index(&storage, None).unwrap();

        let old_results = search_memory(&storage, "outside_bound_1270", 10, None, false).unwrap();
        assert!(old_results
            .iter()
            .all(|result| result.source_ref != "task:task-outside-bound-1270"));
        let recent_results = search_memory(&storage, "recent_bound_499", 10, None, false).unwrap();
        assert!(recent_results
            .iter()
            .any(|result| result.source_ref == "task:task-recent-bound-499"));
    }

    #[test]
    fn memory_get_returns_latest_task_record_for_task_source_ref() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        ensure_agent_home_layout(dir.path()).unwrap();

        storage
            .append_task(&task_record(
                "task-get-1246",
                "default",
                TaskKind::CommandTask,
                TaskStatus::Completed,
                "get compact task receipt",
                Some("wi-get-1246".into()),
                Some(json!({"cmd": "echo task_get"})),
                0,
                0,
            ))
            .unwrap();

        rebuild_memory_index(&storage, None).unwrap();
        let results = search_memory(&storage, "task-get-1246", 10, None, false).unwrap();
        let source_ref = results
            .iter()
            .find(|result| result.source_ref == "task:task-get-1246")
            .expect("task source should be searchable")
            .source_ref
            .clone();
        let memory = get_memory(&storage, &source_ref, None, None)
            .unwrap()
            .expect("task memory source should be gettable");
        assert_eq!(memory.kind, "task");
        assert!(memory.content.contains("task_id: task-get-1246"));
        assert!(memory.content.contains("status: completed"));
        assert!(memory.content.contains("get compact task receipt"));
        assert!(memory.content.contains("work_item_id: wi-get-1246"));
        assert!(memory.content.contains("cmd: echo task_get"));
        assert!(!memory.truncated);
    }

    #[test]
    fn command_receipts_preserve_long_exec_command_input_for_memory_get() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        let command = "python - <<'PY'\nprint('receipt_start')\nprint('sentinel_middle_line_1246')\nprint('receipt_end')\nPY";
        storage
            .append_tool_execution(&ToolExecutionRecord {
                id: "tool-exec-1246".into(),
                agent_id: "default".into(),
                work_item_id: Some("work-1246".into()),
                turn_index: 7,
                tool_name: "ExecCommand".into(),
                created_at: Utc::now(),
                completed_at: Some(Utc::now()),
                duration_ms: 10,
                trust: crate::types::TrustLevel::TrustedOperator,
                status: crate::types::ToolExecutionStatus::Success,
                input: json!({"cmd": command}),
                output: json!({"exit_code": 0}),
                summary: "command exited with status 0".into(),
                invocation_surface: None,
            })
            .unwrap();

        let results = search_memory(
            &storage,
            "sentinel_middle_line_1246",
            10,
            Some("ws-holon"),
            false,
        )
        .unwrap();
        let result = results
            .iter()
            .find(|result| result.kind == "tool_command_receipt")
            .expect("command receipt should be indexed");
        assert_eq!(result.source_ref, "tool_execution:tool-exec-1246:cmd");
        assert_eq!(
            result.metadata["tool_execution_id"].as_str(),
            Some("tool-exec-1246")
        );
        assert_eq!(
            result.metadata["cmd_preview"].as_str(),
            Some("[omitted: command contains heredoc or inline script]")
        );

        let memory = get_memory(&storage, &result.source_ref, None, Some("ws-holon"))
            .unwrap()
            .expect("command receipt should be retrievable");
        assert!(memory.content.contains(command));
        assert!(memory.content.contains("sentinel_middle_line_1246"));
        assert!(!memory.truncated);
    }

    #[test]
    fn command_receipts_preserve_exec_command_batch_item_inputs() {
        let dir = tempdir().unwrap();
        let storage = AppStorage::new(dir.path()).unwrap();
        storage.write_agent(&AgentState::new("default")).unwrap();
        let first_command = "rg -n \"MemorySearch\" src/memory/index.rs";
        let second_command = "node - <<'NODE'\nconsole.log('batch_receipt_middle_1246')\nNODE";
        storage
            .append_tool_execution(&ToolExecutionRecord {
                id: "tool-batch-1246".into(),
                agent_id: "default".into(),
                work_item_id: None,
                turn_index: 8,
                tool_name: "ExecCommandBatch".into(),
                created_at: Utc::now(),
                completed_at: Some(Utc::now()),
                duration_ms: 20,
                trust: crate::types::TrustLevel::TrustedOperator,
                status: crate::types::ToolExecutionStatus::Success,
                input: json!({
                    "items": [
                        {"cmd": first_command},
                        {"cmd": second_command}
                    ]
                }),
                output: json!({"completed_count": 2}),
                summary: "ExecCommandBatch completed 2/2 items".into(),
                invocation_surface: None,
            })
            .unwrap();

        let results = search_memory(
            &storage,
            "batch_receipt_middle_1246",
            10,
            Some("ws-holon"),
            false,
        )
        .unwrap();
        let result = results
            .iter()
            .find(|result| result.source_ref == "tool_execution:tool-batch-1246:batch_item:2:cmd")
            .expect("second batch item receipt should be indexed");
        assert_eq!(result.metadata["batch_item_index"].as_u64(), Some(2));
        assert_eq!(
            result.metadata["cmd_preview"].as_str(),
            Some("[omitted: command contains heredoc or inline script]")
        );
        let memory = get_memory(&storage, &result.source_ref, None, Some("ws-holon"))
            .unwrap()
            .expect("batch command receipt should be retrievable");
        assert!(memory.content.contains(second_command));
        assert!(!memory.content.contains(first_command));
    }
}
