//! Minimal MCP stdio JSON-RPC layer.
//!
//! This crate intentionally implements only the tool server surface required by generated McPact
//! adapters: initialize, tools/list, tools/call, and ping. ADR-0011 tracks the official SDK
//! integration path.

use async_trait::async_trait;
use mcpact_core::{AuthorityClass, TrustCeiling};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::{collections::BTreeMap, sync::Arc};
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};

/// Canonical MCP protocol version this server implements.
///
/// Single source of truth for every layer that needs to name the protocol
/// revision: the `initialize` response, conformance fixtures, and downstream
/// smoke scripts. Bumping the supported revision is a one-line change here
/// plus an entry in [`SUPPORTED_PROTOCOL_VERSIONS`]. ADR-0021 tracks the
/// migration to the official Rust MCP SDK at v0.4; until then this layer is
/// hand-maintained against the spec.
pub const MCP_PROTOCOL_VERSION: &str = "2025-11-25";

/// Every MCP protocol revision this server can speak, newest first.
///
/// Per the MCP lifecycle spec, `initialize` performs version negotiation:
/// the client proposes `params.protocolVersion`; if the server supports that
/// exact revision it echoes it back, otherwise it returns its own latest
/// supported revision and the client decides whether to proceed. The first
/// entry MUST equal [`MCP_PROTOCOL_VERSION`] (enforced by a unit test).
pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2025-11-25"];

/// Negotiate the protocol version for an `initialize` response.
///
/// * `Some(requested)` and the server supports it -> echo the client's value.
/// * `Some(requested)` but unsupported -> fall back to [`MCP_PROTOCOL_VERSION`]
///   (the server's latest), letting the client decide to continue or abort.
/// * `None` (no `protocolVersion` in params) -> assume the server's latest.
///
/// This never errors: an unsupported request degrades to the server default
/// rather than failing the handshake, matching the spec's "respond with a
/// version you do support" guidance.
#[must_use]
pub fn negotiate_protocol_version(requested: Option<&str>) -> &'static str {
    match requested {
        Some(version) => SUPPORTED_PROTOCOL_VERSIONS
            .iter()
            .copied()
            .find(|supported| *supported == version)
            .unwrap_or(MCP_PROTOCOL_VERSION),
        None => MCP_PROTOCOL_VERSION,
    }
}

/// Tool definition exposed through MCP.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ToolDefinition {
    /// Unique MCP tool name.
    pub name: String,
    /// Optional human title.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Description.
    pub description: String,
    /// JSON Schema object for input parameters.
    pub input_schema: Value,
    /// Optional output schema.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output_schema: Option<Value>,
    /// McPact extension metadata.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub annotations: Option<Value>,
}

impl ToolDefinition {
    /// Build standard McPact annotations.
    #[must_use]
    pub fn mcpact_annotations(authority: AuthorityClass, trust: TrustCeiling) -> Value {
        json!({
            "mcpact": {
                "authority": authority,
                "trustCeiling": trust,
                "shellAccess": false
            }
        })
    }
}

/// Result content item.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContentItem {
    /// Content type, normally `text`.
    #[serde(rename = "type")]
    pub content_type: String,
    /// Text content.
    pub text: String,
}

/// MCP tool call result.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallResult {
    /// Result content.
    pub content: Vec<ContentItem>,
    /// True when tool result is an error payload.
    pub is_error: bool,
    /// Structured content extension.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub structured_content: Option<Value>,
}

impl ToolCallResult {
    /// Text success.
    #[must_use]
    pub fn text(text: impl Into<String>) -> Self {
        Self {
            content: vec![ContentItem {
                content_type: "text".into(),
                text: text.into(),
            }],
            is_error: false,
            structured_content: None,
        }
    }

    /// JSON/structured success.
    #[must_use]
    pub fn structured(value: Value) -> Self {
        Self {
            content: vec![ContentItem {
                content_type: "text".into(),
                text: value.to_string(),
            }],
            is_error: false,
            structured_content: Some(value),
        }
    }

    /// Error result.
    #[must_use]
    pub fn error(text: impl Into<String>) -> Self {
        Self {
            content: vec![ContentItem {
                content_type: "text".into(),
                text: text.into(),
            }],
            is_error: true,
            structured_content: None,
        }
    }
}

/// Tool implementation trait.
#[async_trait]
pub trait McpTool: Send + Sync {
    /// Definition shown to MCP clients.
    fn definition(&self) -> ToolDefinition;
    /// Invoke tool with JSON arguments.
    async fn call(&self, arguments: Value) -> ToolCallResult;
}

/// Tool registry.
#[derive(Default, Clone)]
pub struct ToolRegistry {
    tools: BTreeMap<String, Arc<dyn McpTool>>,
}

impl ToolRegistry {
    /// Create empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            tools: BTreeMap::new(),
        }
    }

    /// Register a tool.
    pub fn register<T: McpTool + 'static>(&mut self, tool: T) {
        let name = tool.definition().name;
        self.tools.insert(name, Arc::new(tool));
    }

    /// List tool definitions.
    #[must_use]
    pub fn list(&self) -> Vec<ToolDefinition> {
        self.tools.values().map(|t| t.definition()).collect()
    }

    /// Call tool by name.
    pub async fn call(&self, name: &str, args: Value) -> ToolCallResult {
        match self.tools.get(name) {
            Some(tool) => tool.call(args).await,
            None => ToolCallResult::error(format!("unknown tool `{name}`")),
        }
    }
}

/// JSON-RPC request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
    /// JSON-RPC version.
    pub jsonrpc: String,
    /// Request id, absent for notifications.
    #[serde(default)]
    pub id: Option<Value>,
    /// Method name.
    pub method: String,
    /// Params.
    #[serde(default)]
    pub params: Value,
}

/// JSON-RPC response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
    /// JSON-RPC version.
    pub jsonrpc: String,
    /// Request id.
    pub id: Value,
    /// Result.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<Value>,
    /// Error.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<JsonRpcError>,
}

/// JSON-RPC error object.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
    /// Error code.
    pub code: i32,
    /// Error message.
    pub message: String,
}

/// MCP server error.
#[derive(Debug, Error)]
pub enum McpError {
    /// I/O failed.
    #[error("MCP I/O failed: {0}")]
    Io(#[from] std::io::Error),
    /// JSON failed.
    #[error("MCP JSON failed: {0}")]
    Json(#[from] serde_json::Error),
}

/// Handle a single JSON-RPC request. Notifications return None.
pub async fn handle_request(
    registry: &ToolRegistry,
    req: JsonRpcRequest,
) -> Option<JsonRpcResponse> {
    let id = req.id?;
    let result = match req.method.as_str() {
        "initialize" => {
            let requested = req.params.get("protocolVersion").and_then(Value::as_str);
            json!({
                "protocolVersion": negotiate_protocol_version(requested),
                "capabilities": {"tools": {"listChanged": false}},
                "serverInfo": {"name": "mcpact-generated", "version": env!("CARGO_PKG_VERSION")},
                "mcpact": {
                    "workspaceRoot": std::env::current_dir()
                        .map(|path| path.display().to_string())
                        .unwrap_or_else(|err| format!("unresolved: {err}"))
                }
            })
        }
        "ping" => json!({}),
        "tools/list" => json!({"tools": registry.list()}),
        "tools/call" => {
            let Some(name) = req.params.get("name").and_then(Value::as_str) else {
                return Some(error(id, -32602, "tools/call missing params.name"));
            };
            let args = req
                .params
                .get("arguments")
                .cloned()
                .unwrap_or_else(|| json!({}));
            serde_json::to_value(registry.call(name, args).await)
                .unwrap_or_else(|_| json!({"content": [], "isError": true}))
        }
        _ => {
            return Some(error(
                id,
                -32601,
                format!("method `{}` not found", req.method),
            ))
        }
    };
    Some(JsonRpcResponse {
        jsonrpc: "2.0".into(),
        id,
        result: Some(result),
        error: None,
    })
}

fn error(id: Value, code: i32, message: impl Into<String>) -> JsonRpcResponse {
    JsonRpcResponse {
        jsonrpc: "2.0".into(),
        id,
        result: None,
        error: Some(JsonRpcError {
            code,
            message: message.into(),
        }),
    }
}

/// Serve over stdin/stdout as newline-delimited JSON-RPC.
pub async fn serve_stdio(registry: ToolRegistry) -> Result<(), McpError> {
    let stdin = BufReader::new(tokio::io::stdin());
    let mut stdout = tokio::io::stdout();
    let mut lines = stdin.lines();
    while let Some(line) = lines.next_line().await? {
        if line.trim().is_empty() {
            continue;
        }
        let req = match serde_json::from_str::<JsonRpcRequest>(&line) {
            Ok(req) => req,
            Err(err) => {
                eprintln!("mcpact-mcp: protocol parse error: {err}; line={line}");
                continue;
            }
        };
        if let Some(resp) = handle_request(&registry, req).await {
            let mut encoded = serde_json::to_vec(&resp)?;
            encoded.push(b'\n');
            stdout.write_all(&encoded).await?;
            stdout.flush().await?;
        }
    }
    Ok(())
}

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

    #[derive(Clone)]
    struct Hello;

    #[async_trait]
    impl McpTool for Hello {
        fn definition(&self) -> ToolDefinition {
            ToolDefinition {
                name: "hello".into(),
                title: Some("Hello".into()),
                description: "say hello".into(),
                input_schema: json!({"type":"object","properties":{}}),
                output_schema: None,
                annotations: Some(ToolDefinition::mcpact_annotations(
                    AuthorityClass::Observe,
                    TrustCeiling::Reviewed,
                )),
            }
        }

        async fn call(&self, _arguments: Value) -> ToolCallResult {
            ToolCallResult::text("hello")
        }
    }

    #[tokio::test]
    async fn lists_tools() {
        let mut reg = ToolRegistry::new();
        reg.register(Hello);
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(1)),
            method: "tools/list".into(),
            params: json!({}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        assert_eq!(resp.result.unwrap()["tools"][0]["name"], "hello");
    }

    #[tokio::test]
    async fn calls_tool() {
        let mut reg = ToolRegistry::new();
        reg.register(Hello);
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(2)),
            method: "tools/call".into(),
            params: json!({"name":"hello","arguments":{}}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        assert_eq!(resp.result.unwrap()["content"][0]["text"], "hello");
    }

    #[tokio::test]
    async fn initialize_returns_protocol_version() {
        let reg = ToolRegistry::new();
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(1)),
            method: "initialize".into(),
            params: json!({}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        let result = resp.result.unwrap();
        assert_eq!(result["protocolVersion"], MCP_PROTOCOL_VERSION);
        assert_eq!(
            result["mcpact"]["workspaceRoot"],
            std::env::current_dir().unwrap().display().to_string()
        );
    }

    #[test]
    fn canonical_version_is_first_supported() {
        assert_eq!(
            SUPPORTED_PROTOCOL_VERSIONS.first().copied(),
            Some(MCP_PROTOCOL_VERSION),
            "MCP_PROTOCOL_VERSION must be the newest (first) supported revision"
        );
    }

    #[test]
    fn negotiation_echoes_supported_request() {
        for supported in SUPPORTED_PROTOCOL_VERSIONS {
            assert_eq!(negotiate_protocol_version(Some(supported)), *supported);
        }
    }

    #[test]
    fn negotiation_falls_back_for_unsupported_or_absent() {
        assert_eq!(
            negotiate_protocol_version(Some("1999-01-01")),
            MCP_PROTOCOL_VERSION,
            "unsupported request must degrade to the server's latest version"
        );
        assert_eq!(
            negotiate_protocol_version(None),
            MCP_PROTOCOL_VERSION,
            "absent protocolVersion must default to the server's latest version"
        );
    }

    #[tokio::test]
    async fn initialize_echoes_client_protocol_version() {
        let reg = ToolRegistry::new();
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(1)),
            method: "initialize".into(),
            params: json!({"protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {}}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        assert_eq!(
            resp.result.unwrap()["protocolVersion"],
            MCP_PROTOCOL_VERSION
        );
    }

    #[tokio::test]
    async fn initialize_downgrades_unsupported_client_version() {
        let reg = ToolRegistry::new();
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(1)),
            method: "initialize".into(),
            params: json!({"protocolVersion": "2099-12-31", "capabilities": {}}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        // Spec: respond with a version the server *does* support, do not error.
        assert!(resp.error.is_none());
        assert_eq!(
            resp.result.unwrap()["protocolVersion"],
            MCP_PROTOCOL_VERSION
        );
    }

    #[tokio::test]
    async fn ping_returns_empty_object() {
        let reg = ToolRegistry::new();
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(9)),
            method: "ping".into(),
            params: json!({}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        assert_eq!(resp.result.unwrap(), json!({}));
    }

    #[tokio::test]
    async fn unknown_method_is_error() {
        let reg = ToolRegistry::new();
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(3)),
            method: "nope".into(),
            params: json!({}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        assert_eq!(resp.error.unwrap().code, -32601);
    }

    #[tokio::test]
    async fn tools_call_missing_name_is_error() {
        let reg = ToolRegistry::new();
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: Some(json!(4)),
            method: "tools/call".into(),
            params: json!({}),
        };
        let resp = handle_request(&reg, req).await.unwrap();
        assert_eq!(resp.error.unwrap().code, -32602);
    }

    #[tokio::test]
    async fn fixture_initialize_sequence() {
        let mut reg = ToolRegistry::new();
        reg.register(Hello);
        let fixture = include_str!("../../../fixtures/mcp/initialize.jsonl");
        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
            let req: JsonRpcRequest = serde_json::from_str(line).unwrap();
            if req.method == "notifications/initialized" {
                assert!(req.id.is_none());
                continue;
            }
            let resp = handle_request(&reg, req)
                .await
                .expect("request should return response");
            assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
        }
    }
}
