use sqlx::Row;
#[cfg(feature = "community")]
use std::collections::HashMap;
use crate::mcp::types::AppState;
use std::path::Path;

pub async fn handle_get_item_by_id(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let id = args.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
    match crate::db::get_item_content_with_doc(&state.db_pool, id).await {
        Ok(Some((content, path, mime, _document_id, category))) => Some(serde_json::json!({
            "id": id,
            "content": content,
            "path": path,
            "mime": mime,
            "category": category
        })),
        _ => Some(serde_json::json!({
            "content": [{ "type": "text", "text": format!("Item not found: {}", id) }],
            "isError": true
        })),
    }
}

pub async fn handle_add_item_text(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
    let path_str = crate::db::normalize_document_path(
        args.get("path").and_then(|v| v.as_str()).unwrap_or("unknown"),
    );
    let category = args.get("category").and_then(|v| v.as_str()).unwrap_or("");
    let mut mime_str = args.get("mime").and_then(|v| v.as_str()).map(|s| s.to_string());

    // MIMEタイプが未指定なら拡張子から推測
    if mime_str.is_none() && path_str != "unknown" {
        let path = Path::new(&path_str);
        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
            mime_str = Some(match ext.to_lowercase().as_str() {
                "md" | "markdown" => "text/markdown".to_string(),
                "txt" => "text/plain".to_string(),
                "rs" => "text/x-rust".to_string(),
                "js" | "mjs" => "text/javascript".to_string(),
                "ts" => "text/typescript".to_string(),
                "json" => "application/json".to_string(),
                "html" => "text/html".to_string(),
                "css" => "text/css".to_string(),
                _ => "application/octet-stream".to_string(),
            });
        }
    }

    log::info!(
        "Executing add_item_text (LSA-only): content length={}, path='{}', mime='{:?}'",
        content.chars().count(),
        &path_str,
        mime_str
    );

    // 800文字ずつに分割
    let chars: Vec<char> = content.chars().collect();
    let chunk_strings: Vec<String> = chars
        .chunks(800)
        .map(|chunk| chunk.iter().collect::<String>())
        .collect();

    let mut results = Vec::new();
    
    // 1. ドキュメントレコードの取得または作成
    let doc_id_res = match crate::db::get_document_id_by_path(&state.db_pool, &path_str).await {
        Ok(Some(id)) => {
            if let Some(m) = &mime_str {
                let _ = sqlx::query("UPDATE documents SET mime = ? WHERE id = ? AND (mime IS NULL OR mime != ?)")
                    .bind(m)
                    .bind(id)
                    .bind(m)
                    .execute(&state.db_pool)
                    .await;
            }
            if !category.is_empty() {
                let _ = sqlx::query("UPDATE documents SET category = ? WHERE id = ?")
                    .bind(category)
                    .bind(id)
                    .execute(&state.db_pool)
                    .await;
            }
            Ok(id)
        },
        Ok(None) => {
            match sqlx::query("INSERT INTO documents (path, mime, category) VALUES (?, ?, ?)")
                .bind(&path_str)
                .bind(mime_str)
                .bind(category)
                .execute(&state.db_pool)
                .await
            {
                Ok(res) => Ok(res.last_insert_rowid()),
                Err(e) => Err(serde_json::json!({
                    "content": [{ "type": "text", "text": format!("Failed to create document: {}", e) }],
                    "isError": true
                }))
            }
        },
        Err(e) => Err(serde_json::json!({
            "content": [{ "type": "text", "text": format!("Database error: {}", e) }],
            "isError": true
        }))
    };

    let doc_id = match doc_id_res {
        Ok(id) => id,
        Err(err_json) => {
            return Some(err_json);
        }
    };

    // 2. 既存の同一ドキュメントの全チャンクを削除（上書き）。items_fts も同期して削除する。
    let _ = sqlx::query("DELETE FROM items_fts WHERE rowid IN (SELECT id FROM items WHERE document_id = ?)")
        .bind(doc_id)
        .execute(&state.db_pool)
        .await;
    if let Err(e) = sqlx::query("DELETE FROM items WHERE document_id = ?")
        .bind(doc_id)
        .execute(&state.db_pool)
        .await
    {
        log::error!("Failed to delete old chunks for document {}: {}", doc_id, e);
    }

    // 3. 各チャンクを保存
    for (idx, chunk_content) in chunk_strings.iter().enumerate() {
        match add_item_chunk_inner(state, doc_id, idx as i32, chunk_content).await {
            Ok(id) => results.push(id),
            Err(e) => log::error!("Failed to add chunk {}: {}", idx, e),
        }
    }

    if !results.is_empty() {
        let _ = state.tx.send("data_changed".to_string());
        crate::mcp::system::schedule_retrain_if_needed(state).await;
        log::info!("Successfully added {} chunks to document {}.", results.len(), path_str);
        Some(
            serde_json::json!({ "content": [{ "type": "text", "text": format!("Successfully added {} chunks for {}", results.len(), path_str) }] }),
        )
    } else {
        Some(serde_json::json!({
            "content": [{ "type": "text", "text": "Failed to add any chunks." }],
            "isError": true
        }))
    }
}

async fn add_item_chunk_inner(
    state: &AppState,
    doc_id: i64,
    chunk_index: i32,
    content: &str,
) -> Result<i64, String> {
    let mut tx =
        state.db_pool.begin().await.map_err(|e| {
            format!("Failed to begin transaction: {}", e)
        })?;
    
    let res =
        sqlx::query("INSERT INTO items (document_id, chunk_index, content) VALUES (?, ?, ?)")
            .bind(doc_id)
            .bind(chunk_index)
            .bind(content)
            .execute(&mut *tx)
            .await
            .map_err(|e| format!("Failed to insert chunk: {}", e))?;
    let id = res.last_insert_rowid();

    // FTS5 への保存
    sqlx::query("INSERT INTO items_fts (rowid, content) VALUES (?, ?)")
        .bind(id)
        .bind(content)
        .execute(&mut *tx)
        .await
        .map_err(|e| format!("Failed to insert to FTS: {}", e))?;

    #[cfg(feature = "community")]
    let mut lsa_vector_f32: Vec<f32> = vec![0.0; 50];
    #[cfg(feature = "community")]
    let lsa_guard = state.lsa_model.read().await;
    #[cfg(feature = "community")]
    if let Some(model) = lsa_guard.as_ref() {
        let mut query_counts = HashMap::new();
        let tokens = state.tokenizer.tokenize_to_vec(content).unwrap_or_default();
        for token in tokens {
            if let Some(&tid) = model.vocabulary.get(&token) {
                *query_counts.entry(tid).or_insert(0.0) += 1.0;
            }
        }
        let mut query_vec = ndarray::Array1::zeros(model.vocabulary.len());
        for (tid, count) in query_counts {
            query_vec[tid] = count;
        }

        if let Ok(projected) = model.project_query(&query_vec) {
            lsa_vector_f32 = projected.iter().map(|&x| x as f32).collect();
            if lsa_vector_f32.len() < 50 {
                lsa_vector_f32.resize(50, 0.0);
            } else if lsa_vector_f32.len() > 50 {
                lsa_vector_f32.truncate(50);
            }
        }
    }

    #[cfg(feature = "community")]
    {
        sqlx::query("INSERT INTO vec_items (id, embedding) VALUES (?, ?)")
            .bind(id)
            .bind(serde_json::to_string(&lsa_vector_f32).unwrap_or("[]".to_string()))
            .execute(&mut *tx)
            .await
            .map_err(|e| format!("Failed to insert LSA vector to vec_items: {}", e))?;
        if lsa_guard.as_ref().is_some() {
            let vector_blob = bincode::serialize(&lsa_vector_f32).unwrap_or_default();
            sqlx::query("INSERT INTO items_lsa (id, vector) VALUES (?, ?)")
                .bind(id)
                .bind(vector_blob)
                .execute(&mut *tx)
                .await
                .map_err(|e| format!("Failed to insert LSA blob: {}", e))?;
        }
    }

    #[cfg(feature = "pro")]
    let pro_embedding: Option<Vec<f32>> = {
        let guard = state.embedding_model.read().await;
        guard.as_ref().and_then(|model| model.encode(content).ok())
    };
    #[cfg(feature = "pro")]
    if let Some(ref embedding) = pro_embedding {
        let emb_json = serde_json::to_string(embedding).unwrap_or_else(|_| "[]".to_string());
        let _ = sqlx::query("INSERT INTO vec_items (id, embedding) VALUES (?, ?)")
            .bind(id)
            .bind(&emb_json)
            .execute(&mut *tx)
            .await;
    }

    tx.commit()
        .await
        .map_err(|e| format!("Failed to commit transaction: {}", e))?;

    #[cfg(feature = "community")]
    {
        let hnsw_index_guard = state.hnsw_index.read().await;
        if let Some(hnsw_ptr) = hnsw_index_guard.as_ref() {
            let vec_ref: &[f32] = lsa_vector_f32.as_slice();
            hnsw_ptr.insert((vec_ref, id as usize));
        }
    }

    #[cfg(feature = "pro")]
    if let Some(ref embedding) = pro_embedding {
        let hnsw_index_guard = state.hnsw_index.read().await;
        if let Some(hnsw_ptr) = hnsw_index_guard.as_ref() {
            let vec_ref: &[f32] = embedding.as_slice();
            hnsw_ptr.insert((vec_ref, id as usize));
        }
    }

    Ok(id)
}

pub async fn handle_update_item(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let id = args.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
    let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");

    if let Err(e) = update_item_inner(state, id, content).await {
        Some(serde_json::json!({
            "content": [{ "type": "text", "text": format!("Error: {}", e) }],
            "isError": true
        }))
    } else {
        let _ = state.tx.send("data_changed".to_string());
        crate::mcp::system::schedule_retrain_if_needed(state).await;
        Some(
            serde_json::json!({ "content": [{ "type": "text", "text": format!("Successfully updated item {} (LSA)", id) }] }),
        )
    }
}

async fn update_item_inner(
    state: &AppState,
    id: i64,
    content: &str,
) -> Result<(), String> {
    let mut tx = state.db_pool.begin().await.map_err(|e| format!("Failed to begin transaction: {}", e))?;
    
    sqlx::query("UPDATE items SET content = ? WHERE id = ?")
        .bind(content)
        .bind(id)
        .execute(&mut *tx)
        .await
        .map_err(|e| format!("Failed to update item: {}", e))?;

    sqlx::query("UPDATE items_fts SET content = ? WHERE rowid = ?")
        .bind(content)
        .bind(id)
        .execute(&mut *tx)
        .await
        .map_err(|e| format!("Failed to update FTS: {}", e))?;

    #[cfg(feature = "pro")]
    {
        let guard = state.embedding_model.read().await;
        if let Some(ref model) = *guard {
            if let Ok(embedding) = model.encode(content) {
                let emb_json = serde_json::to_string(&embedding).unwrap_or_else(|_| "[]".to_string());
                let _ = sqlx::query("INSERT OR REPLACE INTO vec_items (id, embedding) VALUES (?, ?)")
                    .bind(id)
                    .bind(&emb_json)
                    .execute(&mut *tx)
                    .await;
            }
        }
    }

    #[cfg(feature = "community")]
    {
        let lsa_guard = state.lsa_model.read().await;
        if let Some(model) = lsa_guard.as_ref() {
            let mut query_counts = HashMap::new();
            let tokens = state.tokenizer.tokenize_to_vec(content).unwrap_or_default();
            for token in tokens {
                if let Some(&tid) = model.vocabulary.get(&token) {
                    *query_counts.entry(tid).or_insert(0.0) += 1.0;
                }
            }
            let mut query_vec = ndarray::Array1::zeros(model.vocabulary.len());
            for (tid, count) in query_counts {
                query_vec[tid] = count;
            }

            if let Ok(projected) = model.project_query(&query_vec) {
                let mut proj_f32: Vec<f32> = projected.iter().map(|&x| x as f32).collect();
                if proj_f32.len() < 50 { proj_f32.resize(50, 0.0); } else { proj_f32.truncate(50); }

                let vector_blob = bincode::serialize(&proj_f32).unwrap_or_default();
                sqlx::query("INSERT OR REPLACE INTO items_lsa (id, vector) VALUES (?, ?)")
                    .bind(id)
                    .bind(vector_blob)
                    .execute(&mut *tx)
                    .await
                    .map_err(|e| format!("Failed to update LSA vector: {}", e))?;

                sqlx::query("INSERT OR REPLACE INTO vec_items (id, embedding) VALUES (?, ?)")
                    .bind(id)
                    .bind(serde_json::to_string(&proj_f32).unwrap_or("[]".to_string()))
                    .execute(&mut *tx)
                    .await
                    .map_err(|e| format!("Failed to update vec_items: {}", e))?;
            }
        }
    }

    tx.commit().await.map_err(|e| format!("Failed to commit transaction: {}", e))?;
    Ok(())
}

pub async fn handle_delete_item(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let id = args.get("id").and_then(|v| v.as_i64()).unwrap_or(0);

    if let Err(e) = delete_item_inner(state, id).await {
        Some(serde_json::json!({
            "content": [{ "type": "text", "text": format!("Error: {}", e) }],
            "isError": true
        }))
    } else {
        let _ = state.tx.send("data_changed".to_string());
        crate::mcp::system::schedule_retrain_if_needed(state).await;
        Some(
            serde_json::json!({ "content": [{ "type": "text", "text": format!("Successfully deleted item {}", id) }] }),
        )
    }
}

async fn delete_item_inner(state: &AppState, id: i64) -> Result<(), String> {
    let mut tx = state.db_pool.begin().await.map_err(|e| format!("Failed to begin transaction: {}", e))?;
    sqlx::query("DELETE FROM items WHERE id = ?").bind(id).execute(&mut *tx).await.map_err(|e| format!("Failed to delete item: {}", e))?;
    sqlx::query("DELETE FROM vec_items WHERE id = ?").bind(id).execute(&mut *tx).await.map_err(|e| format!("Failed to delete vector: {}", e))?;
    sqlx::query("DELETE FROM items_fts WHERE rowid = ?").bind(id).execute(&mut *tx).await.map_err(|e| format!("Failed to delete from FTS: {}", e))?;
    tx.commit().await.map_err(|e| format!("Failed to commit transaction: {}", e))?;
    Ok(())
}

// ----------------------------------------------------------------------------
// Document count (Issue #5) — documents テーブルの件数を返す
// ----------------------------------------------------------------------------

pub async fn handle_get_document_count(state: &AppState) -> Option<serde_json::Value> {
    let count: i64 = match crate::db::get_document_count(&state.db_pool).await {
        Ok(c) => c,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Failed to get document count: {}", e) }],
                "isError": true
            }));
        }
    };
    let text = serde_json::to_string_pretty(&serde_json::json!({ "count": count }))
        .unwrap_or_else(|_| format!("{{\"count\": {}}}", count));
    Some(serde_json::json!({
        "content": [{ "type": "text", "text": text }]
    }))
}

// ----------------------------------------------------------------------------
// Document-level API (list, get full content, delete by document)
// ----------------------------------------------------------------------------

/// ドキュメント一覧をページングで返す。limit（1ページあたり件数）・page（1始まり）を指定。範囲外のページは items: [] で返す。
pub async fn handle_list_documents(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let limit = args
        .get("limit")
        .and_then(|v| v.as_i64())
        .unwrap_or(20)
        .clamp(1, 100);
    let page = args
        .get("page")
        .and_then(|v| v.as_i64())
        .unwrap_or(1)
        .max(1);
    let offset = (page - 1) * limit;

    let total_count: i64 = match sqlx::query_scalar("SELECT COUNT(*) FROM documents")
        .fetch_one(&state.db_pool)
        .await
    {
        Ok(n) => n,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Failed to count documents: {}", e) }],
                "isError": true
            }));
        }
    };

    let total_pages = if total_count == 0 {
        0
    } else {
        ((total_count as f64) / (limit as f64)).ceil() as i64
    };

    let rows = match sqlx::query(
        "SELECT d.id, d.path, d.mime, d.updated_at,
                (SELECT COUNT(*) FROM items i WHERE i.document_id = d.id) AS chunk_count,
                (SELECT substr(i.content, 1, 15) FROM items i WHERE i.document_id = d.id ORDER BY i.chunk_index ASC LIMIT 1) AS chunk0_preview,
                COALESCE(d.category, '') AS category
         FROM documents d ORDER BY d.path LIMIT ? OFFSET ?",
    )
    .bind(limit)
    .bind(offset)
    .fetch_all(&state.db_pool)
    .await
    {
        Ok(r) => r,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Failed to list documents: {}", e) }],
                "isError": true
            }));
        }
    };

    let items: Vec<serde_json::Value> = rows.iter().map(|row| {
        let id: i64 = row.get(0);
        let path: String = row.get(1);
        let mime: Option<String> = row.get(2);
        let updated_at: Option<String> = row.get(3);
        let chunk_count: i64 = row.get(4);
        let chunk0_preview: Option<String> = row.get(5);
        let category: String = row.get(6);
        serde_json::json!({
            "id": id,
            "path": path,
            "mime": mime,
            "updated_at": updated_at,
            "chunk_count": chunk_count,
            "chunk0_preview": chunk0_preview,
            "category": category
        })
    }).collect();

    let payload = serde_json::json!({
        "items": items,
        "total_count": total_count,
        "total_pages": total_pages,
        "page": page
    });
    let text = serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string());
    Some(serde_json::json!({
        "content": [{ "type": "text", "text": text }]
    }))
}

/// ドキュメントに設定されているカテゴリ名の一覧（重複なし・空でないもの）を返す。
pub async fn handle_list_categories(state: &AppState) -> Option<serde_json::Value> {
    let rows = match sqlx::query_scalar::<_, String>(
        "SELECT DISTINCT COALESCE(category, '') FROM documents WHERE COALESCE(category, '') != '' ORDER BY category",
    )
    .fetch_all(&state.db_pool)
    .await
    {
        Ok(r) => r,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Failed to list categories: {}", e) }],
                "isError": true
            }));
        }
    };
    let text = serde_json::to_string_pretty(&rows).unwrap_or_else(|_| "[]".to_string());
    Some(serde_json::json!({
        "content": [{ "type": "text", "text": text }]
    }))
}

pub async fn handle_get_document(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let doc_id = args.get("document_id").and_then(|v| v.as_i64())
        .or_else(|| args.get("id").and_then(|v| v.as_i64()))
        .unwrap_or(0);

    let doc_row = match sqlx::query("SELECT id, path, mime FROM documents WHERE id = ?")
        .bind(doc_id)
        .fetch_optional(&state.db_pool)
        .await
    {
        Ok(Some(r)) => r,
        Ok(None) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Document not found: {}", doc_id) }],
                "isError": true
            }));
        }
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Error: {}", e) }],
                "isError": true
            }));
        }
    };

    let path: String = doc_row.get(1);
    let mime: Option<String> = doc_row.get(2);

    let chunks = match sqlx::query("SELECT id, chunk_index, content FROM items WHERE document_id = ? ORDER BY chunk_index")
        .bind(doc_id)
        .fetch_all(&state.db_pool)
        .await
    {
        Ok(rows) => rows,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Error: {}", e) }],
                "isError": true
            }));
        }
    };

    let full_content: String = chunks.iter()
        .map(|r| r.get::<String, _>(2))
        .collect();

    let chunks_json: Vec<serde_json::Value> = chunks.iter().map(|r| {
        serde_json::json!({
            "id": r.get::<i64, _>(0),
            "chunk_index": r.get::<i32, _>(1),
            "content": r.get::<String, _>(2)
        })
    }).collect();

    Some(serde_json::json!({
        "content": [{
            "type": "text",
            "text": serde_json::to_string_pretty(&serde_json::json!({
                "id": doc_id,
                "path": path,
                "mime": mime,
                "content": full_content,
                "chunks": chunks_json
            })).unwrap_or_else(|_| "{}".to_string())
        }]
    }))
}

pub async fn handle_delete_document(
    state: &AppState,
    args: &serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Value> {
    let doc_id = args.get("document_id").and_then(|v| v.as_i64())
        .or_else(|| args.get("id").and_then(|v| v.as_i64()))
        .unwrap_or(0);

    let item_ids: Vec<i64> = match sqlx::query_scalar("SELECT id FROM items WHERE document_id = ?")
        .bind(doc_id)
        .fetch_all(&state.db_pool)
        .await
    {
        Ok(ids) => ids,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Error: {}", e) }],
                "isError": true
            }));
        }
    };

    let mut tx = match state.db_pool.begin().await {
        Ok(t) => t,
        Err(e) => {
            return Some(serde_json::json!({
                "content": [{ "type": "text", "text": format!("Error: {}", e) }],
                "isError": true
            }));
        }
    };

    for id in &item_ids {
        let _ = sqlx::query("DELETE FROM vec_items WHERE id = ?").bind(id).execute(&mut *tx).await;
        let _ = sqlx::query("DELETE FROM items_fts WHERE rowid = ?").bind(id).execute(&mut *tx).await;
        let _ = sqlx::query("DELETE FROM items_lsa WHERE id = ?").bind(id).execute(&mut *tx).await;
    }
    let _ = sqlx::query("DELETE FROM items WHERE document_id = ?").bind(doc_id).execute(&mut *tx).await;
    if let Err(e) = sqlx::query("DELETE FROM documents WHERE id = ?").bind(doc_id).execute(&mut *tx).await {
        let _ = tx.rollback().await;
        return Some(serde_json::json!({
            "content": [{ "type": "text", "text": format!("Error: {}", e) }],
            "isError": true
        }));
    }
    if let Err(e) = tx.commit().await {
        return Some(serde_json::json!({
            "content": [{ "type": "text", "text": format!("Error: {}", e) }],
            "isError": true
        }));
    }

    let _ = state.tx.send("data_changed".to_string());
    crate::mcp::system::schedule_retrain_if_needed(state).await;
    Some(serde_json::json!({
        "content": [{ "type": "text", "text": format!("Successfully deleted document {} ({} chunks)", doc_id, item_ids.len()) }]
    }))
}

// ---------------------------------------------------------------------------
// フォルダ監視用: ファイルパスから取り込み・パス指定削除（計画 folder_monitor）
// ---------------------------------------------------------------------------

/// 指定パスのファイルを UTF-8 テキストとして読み、add_item_text と同様に DB に取り込む。
pub async fn ingest_file_path(state: &AppState, path: &Path, category: &str) -> Result<(), String> {
    let path_str = path.to_string_lossy().to_string();
    let content = std::fs::read_to_string(path).map_err(|e| format!("read file: {}", e))?;
    let mut args = serde_json::Map::new();
    args.insert("content".to_string(), serde_json::Value::String(content));
    args.insert("path".to_string(), serde_json::Value::String(path_str.clone()));
    if !category.is_empty() {
        args.insert("category".to_string(), serde_json::Value::String(category.to_string()));
    }
    match handle_add_item_text(state, &args).await {
        Some(v) if v.get("isError").and_then(|x| x.as_bool()).unwrap_or(false) => {
            let msg = v
                .get("content")
                .and_then(|c| c.as_array())
                .and_then(|a| a.first())
                .and_then(|o| o.get("text"))
                .and_then(|t| t.as_str())
                .unwrap_or("unknown error");
            Err(msg.to_string())
        }
        Some(_) => Ok(()),
        None => Err("add_item_text returned None".to_string()),
    }
}

/// パスに紐づくドキュメントを削除する。存在しなければ Ok(())。
pub async fn delete_document_by_path(state: &AppState, path: &str) -> Result<(), String> {
    let doc_id = match crate::db::get_document_id_by_path(&state.db_pool, path).await? {
        Some(id) => id,
        None => return Ok(()),
    };
    let mut args = serde_json::Map::new();
    args.insert("document_id".to_string(), serde_json::Value::Number(serde_json::Number::from(doc_id)));
    if let Some(v) = handle_delete_document(state, &args).await {
        if v.get("isError").and_then(|x| x.as_bool()).unwrap_or(false) {
            let msg = v
                .get("content")
                .and_then(|c| c.as_array())
                .and_then(|a| a.first())
                .and_then(|o| o.get("text"))
                .and_then(|t| t.as_str())
                .unwrap_or("unknown error");
            return Err(msg.to_string());
        }
    }
    Ok(())
}

/// 指定ディレクトリ配下のドキュメントをすべてインデックスから削除する（モニター解除時に「インデックスからも削除」で使用）。
pub async fn delete_documents_under_path_prefix(state: &AppState, path_prefix: &str) -> Result<u32, String> {
    let doc_ids = crate::db::get_document_ids_under_path_prefix(&state.db_pool, path_prefix).await?;
    let mut count = 0u32;
    for doc_id in doc_ids {
        let mut args = serde_json::Map::new();
        args.insert("document_id".to_string(), serde_json::Value::Number(serde_json::Number::from(doc_id)));
        if let Some(v) = handle_delete_document(state, &args).await {
            if !v.get("isError").and_then(|x| x.as_bool()).unwrap_or(false) {
                count += 1;
            }
        }
    }
    Ok(count)
}
