//! MCP diagnostic tools — server logs, CRDT dump, version, line counting, story movement.

use crate::agents::move_story_to_stage;
use crate::http::context::AppContext;
use crate::log_buffer;
use crate::slog;
use serde_json::{Value, json};

mod permission;
mod usage;

pub(crate) use permission::tool_prompt_permission;
pub(crate) use usage::tool_get_token_usage;

pub(crate) fn tool_get_server_logs(args: &Value) -> Result<String, String> {
    let lines_count = args
        .get("lines")
        .and_then(|v| v.as_u64())
        .map(|n| n.min(1000) as usize)
        .unwrap_or(100);
    let filter = args.get("filter").and_then(|v| v.as_str());
    let severity = args
        .get("severity")
        .and_then(|v| v.as_str())
        .and_then(log_buffer::LogLevel::from_str_ci);

    let recent = log_buffer::global().get_recent(lines_count, filter, severity.as_ref());
    let joined = recent.join("\n");
    // Clamp to lines_count actual lines in case any entry contains embedded newlines.
    let all_lines: Vec<&str> = joined.lines().collect();
    let start = all_lines.len().saturating_sub(lines_count);
    Ok(all_lines[start..].join("\n"))
}

/// Rebuild the server binary and re-exec (delegates to `crate::rebuild`).
pub(crate) async fn tool_rebuild_and_restart(ctx: &AppContext) -> Result<String, String> {
    slog!("[rebuild] Rebuild and restart requested via MCP tool");

    // Signal the Matrix bot (if active) so it can send its own shutdown
    // announcement before the process is replaced.  Best-effort: we wait up
    // to 1.5 s for the message to be delivered.
    if let Some(ref tx) = ctx.matrix_shutdown_tx {
        let _ = tx.send(Some(crate::rebuild::ShutdownReason::Rebuild));
        tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
    }

    let project_root = ctx.state.get_project_root().unwrap_or_default();
    let notifier = ctx.bot_shutdown.as_deref();
    crate::rebuild::rebuild_and_restart(&ctx.services.agents, &project_root, notifier).await
}

/// MCP tool called by Claude Code via `--permission-prompt-tool`.
///
/// Forwards the permission request through the shared channel to the active
/// WebSocket session, which presents a dialog to the user. Blocks until the
/// user approves or denies (with a 5-minute timeout).
pub(crate) fn tool_move_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let target_stage = args
        .get("target_stage")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: target_stage")?;

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;

    let (from_stage, to_stage) = move_story_to_stage(&project_root, story_id, target_stage)?;

    serde_json::to_string_pretty(&json!({
        "story_id": story_id,
        "from_stage": from_stage,
        "to_stage": to_stage,
        "message": format!("Work item '{story_id}' moved from '{from_stage}' to '{to_stage}'.")
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// MCP tool: dump the raw in-memory CRDT state for debugging.
///
/// **Debug tool only** — for normal pipeline introspection use `get_pipeline_status`.
pub(crate) fn tool_dump_crdt(args: &Value) -> Result<String, String> {
    let story_id_filter = args.get("story_id").and_then(|v| v.as_str());
    let dump = crate::crdt_state::dump_crdt_state(story_id_filter);

    let items: Vec<Value> = dump
        .items
        .into_iter()
        .map(|item| {
            json!({
                "story_id": item.story_id,
                "stage": item.stage,
                "name": item.name,
                "agent": item.agent,
                "retry_count": item.retry_count,
                "blocked": item.blocked,
                "depends_on": item.depends_on,
                "claimed_by": item.claimed_by,
                "claimed_at": item.claimed_at,
                "content_index": item.content_index,
                "is_deleted": item.is_deleted,
            })
        })
        .collect();

    serde_json::to_string_pretty(&json!({
        "metadata": {
            "in_memory_state_loaded": dump.in_memory_state_loaded,
            "total_items": dump.total_items,
            "total_ops_in_list": dump.total_ops_in_list,
            "max_seq_in_list": dump.max_seq_in_list,
            "persisted_ops_count": dump.persisted_ops_count,
            "pending_persist_ops_count": null,
        },
        "items": items,
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// MCP tool: return the server version, build hash, and running port.
pub(crate) fn tool_get_version(ctx: &AppContext) -> Result<String, String> {
    let build_hash =
        std::fs::read_to_string(".huskies/build_hash").unwrap_or_else(|_| "unknown".to_string());
    serde_json::to_string_pretty(&json!({
        "version": env!("CARGO_PKG_VERSION"),
        "build_hash": build_hash.trim(),
        "port": ctx.services.agents.port(),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// MCP tool: count lines in a specific file relative to the project root.
pub(crate) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let file_path = args
        .get("file_path")
        .and_then(|v| v.as_str())
        .ok_or_else(|| "Missing required argument: file_path".to_string())?;

    let project_root = ctx.state.get_project_root()?;
    Ok(crate::chat::commands::loc::loc_single_file(
        &project_root,
        file_path,
    ))
}

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

    #[test]
    fn tool_get_server_logs_no_args_returns_string() {
        let result = tool_get_server_logs(&json!({})).unwrap();
        // Returns recent log lines (possibly empty in tests) — just verify no panic
        let _ = result;
    }

    #[test]
    fn tool_get_server_logs_with_filter_returns_matching_lines() {
        let result = tool_get_server_logs(&json!({"filter": "xyz_unlikely_match_999"})).unwrap();
        assert_eq!(
            result, "",
            "filter with no matches should return empty string"
        );
    }

    #[test]
    fn tool_get_server_logs_with_line_limit() {
        let result = tool_get_server_logs(&json!({"lines": 5})).unwrap();
        assert!(result.lines().count() <= 5);
    }

    #[test]
    fn tool_get_server_logs_max_cap_is_1000() {
        // Lines > 1000 are capped — just verify it returns without error
        let result = tool_get_server_logs(&json!({"lines": 9999})).unwrap();
        let _ = result;
    }
}
