//! Minimal **JSON-RPC 2.0** message model + transport seam for the IdeA MCP
//! adapter (spike **S-MCP**).
//!
//! ## Why hand-rolled JSON-RPC instead of an MCP crate
//!
//! MCP (Model Context Protocol) rides on JSON-RPC 2.0: a server advertises its
//! tools via `tools/list` and runs them via `tools/call`, after an `initialize`
//! handshake. The surface IdeA needs is exactly those three methods — no
//! resources, no prompts, no sampling. The Rust MCP crate ecosystem
//! (`rmcp`, `mcp-sdk`, …) is young, churny, and drags in a concrete async
//! transport/runtime stack that is awkward to drive **without a socket or child
//! process** in a unit test. The cadrage (S-MCP) explicitly permits implementing
//! "the strict JSON-RPC 2.0 minimum (`initialize`, `tools/list`, `tools/call`)"
//! ourselves when that buys **testability and zero-network**. We take that path:
//! the whole protocol lives in this adapter, behind a tiny [`Transport`] trait, so
//! tests speak it over an in-memory transport with no I/O at all.
//!
//! Everything here is JSON-RPC plumbing — it never knows what an
//! `OrchestratorCommand` is. The mapping to IdeA semantics lives in
//! [`super::server`].

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// The JSON-RPC protocol version string every message must carry.
pub const JSONRPC_VERSION: &str = "2.0";

/// A request id as defined by JSON-RPC 2.0: a string, a number, or null. We keep
/// it as a raw [`Value`] and echo it back verbatim on the matching response so the
/// transport-level correlation (Décision 2: corrélation portée par le transport)
/// is preserved exactly, whatever the client chose.
pub type RequestId = Value;

/// An incoming JSON-RPC request (or notification, when `id` is absent).
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct JsonRpcRequest {
    /// Must equal [`JSONRPC_VERSION`].
    pub jsonrpc: String,
    /// Correlation id. Absent ⇒ the message is a *notification* (no reply owed).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<RequestId>,
    /// The method name (`initialize`, `tools/list`, `tools/call`, …).
    pub method: String,
    /// Method parameters; shape depends on `method`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,
}

/// An outgoing JSON-RPC response: exactly one of `result` / `error` is set.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct JsonRpcResponse {
    /// Must equal [`JSONRPC_VERSION`].
    pub jsonrpc: String,
    /// Echoes the request id (null for errors that could not be correlated).
    pub id: RequestId,
    /// Success payload (mutually exclusive with [`Self::error`]).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<Value>,
    /// Failure payload (mutually exclusive with [`Self::result`]).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<JsonRpcError>,
}

impl JsonRpcResponse {
    /// Builds a success response echoing `id`.
    #[must_use]
    pub fn success(id: RequestId, result: Value) -> Self {
        Self {
            jsonrpc: JSONRPC_VERSION.to_owned(),
            id,
            result: Some(result),
            error: None,
        }
    }

    /// Builds an error response echoing `id`.
    #[must_use]
    pub fn error(id: RequestId, error: JsonRpcError) -> Self {
        Self {
            jsonrpc: JSONRPC_VERSION.to_owned(),
            id,
            result: None,
            error: Some(error),
        }
    }
}

/// A JSON-RPC error object.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct JsonRpcError {
    /// One of the standard JSON-RPC codes (see [`error_codes`]).
    pub code: i32,
    /// Short human-readable description.
    pub message: String,
    /// Optional structured detail.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
}

impl JsonRpcError {
    /// Builds an error with no `data`.
    #[must_use]
    pub fn new(code: i32, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into(),
            data: None,
        }
    }
}

/// The subset of standard JSON-RPC 2.0 error codes this adapter raises.
pub mod error_codes {
    /// Invalid JSON was received by the server.
    pub const PARSE_ERROR: i32 = -32_700;
    /// The JSON sent is not a valid Request object.
    pub const INVALID_REQUEST: i32 = -32_600;
    /// The method does not exist / is not supported.
    pub const METHOD_NOT_FOUND: i32 = -32_601;
    /// Invalid method parameters.
    pub const INVALID_PARAMS: i32 = -32_602;
    /// Internal server error (a dispatched IdeA command failed).
    pub const INTERNAL_ERROR: i32 = -32_603;
}

/// Errors a [`Transport`] may surface.
#[derive(Debug, thiserror::Error)]
pub enum TransportError {
    /// The peer closed the stream (clean EOF). The serve loop stops on this.
    #[error("transport closed")]
    Closed,
    /// An underlying I/O failure.
    #[error("transport io error: {0}")]
    Io(String),
}

/// Abstract message transport for the MCP server (the **S-MCP transport seam**).
///
/// One framed JSON-RPC message per `recv`/`send`. Concrete transports (stdio, and
/// later a socket) live behind this trait so the server's protocol logic is
/// **transport-agnostic** and, crucially, **unit-testable over an in-memory
/// transport** with no socket and no child process. The `stdio` transport is the
/// default concrete one; `socket` is a documented TODO (cadrage S-MCP).
#[async_trait::async_trait]
pub trait Transport: Send {
    /// Receives the next framed message as raw bytes. Returns
    /// [`TransportError::Closed`] on clean EOF (the serve loop exits).
    async fn recv(&mut self) -> Result<Vec<u8>, TransportError>;

    /// Sends one framed message (raw bytes, a complete JSON value).
    async fn send(&mut self, message: &[u8]) -> Result<(), TransportError>;
}

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

    #[test]
    fn request_without_id_is_a_notification() {
        let raw = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
        let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
        assert!(req.id.is_none());
        assert_eq!(req.method, "notifications/initialized");
    }

    #[test]
    fn success_and_error_are_mutually_exclusive_on_the_wire() {
        let ok = JsonRpcResponse::success(serde_json::json!(1), serde_json::json!({"k":"v"}));
        let text = serde_json::to_string(&ok).unwrap();
        assert!(text.contains("\"result\""));
        assert!(!text.contains("\"error\""));

        let err = JsonRpcResponse::error(
            serde_json::json!(1),
            JsonRpcError::new(error_codes::METHOD_NOT_FOUND, "nope"),
        );
        let text = serde_json::to_string(&err).unwrap();
        assert!(text.contains("\"error\""));
        assert!(!text.contains("\"result\""));
    }
}
