//! JSON-RPC envelope and protocol-version types.
//!
//! Why a dedicated module: serde-deriving the envelope in one place
//! keeps the wire shape canonical. Callers pass `serde_json::Value`
//! payloads through the Client; the envelope handles request/response/
//! notification routing entirely on the IDs.

use serde::{Deserialize, Serialize};

use super::ProtocolError;

/// MCP protocol versions accepted by the v1 engine.
///
/// We pin to the published or drafted revisions, matching ADR-0002.
/// The string form on the wire is the `YYYY-MM-DD` date stamp; the
/// engine refuses any other value at the handshake step.
///
/// 2026-07-28 introduces the stateless transport: no
/// `initialize` / `initialized` handshake, no `Mcp-Session-Id`,
/// protocol version + client info + capabilities ride in `_meta` on
/// every request, and `server/discover` returns capabilities on
/// demand. The pre-2026-07-28 path stays exactly as it was.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProtocolVersion {
    /// First public revision.
    V2024_11_05,
    /// First stabilization revision.
    V2025_03_26,
    /// Production revision (server SDK default).
    V2025_06_18,
    /// Late 2025 revision: last revision in the session-based shape
    /// before the stateless cut-over.
    V2025_11_25,
    /// Spring 2026 draft tracked between releases.
    V2026_03_26,
    /// Summer 2026 revision: introduces the stateless transport.
    V2026_07_28,
}

impl ProtocolVersion {
    /// Wire string the spec uses on the wire.
    pub const fn as_wire(self) -> &'static str {
        match self {
            ProtocolVersion::V2024_11_05 => "2024-11-05",
            ProtocolVersion::V2025_03_26 => "2025-03-26",
            ProtocolVersion::V2025_06_18 => "2025-06-18",
            ProtocolVersion::V2025_11_25 => "2025-11-25",
            ProtocolVersion::V2026_03_26 => "2026-03-26",
            ProtocolVersion::V2026_07_28 => "2026-07-28",
        }
    }

    /// Parse a wire string. Returns `None` for any unsupported version
    /// so the caller can lift it into a [`ProtocolError::ProtocolVersionMismatch`].
    pub fn from_wire(s: &str) -> Option<Self> {
        match s {
            "2024-11-05" => Some(ProtocolVersion::V2024_11_05),
            "2025-03-26" => Some(ProtocolVersion::V2025_03_26),
            "2025-06-18" => Some(ProtocolVersion::V2025_06_18),
            "2025-11-25" => Some(ProtocolVersion::V2025_11_25),
            "2026-03-26" => Some(ProtocolVersion::V2026_03_26),
            "2026-07-28" => Some(ProtocolVersion::V2026_07_28),
            _ => None,
        }
    }

    /// Default version the Client advertises during handshake when the
    /// caller does not pin one. Chosen as the current production
    /// revision (mirrors what most server SDKs default to). Held at the
    /// last session-shape revision so callers that do not opt in to
    /// 2026-07-28 stay on the well-trodden handshake path; flip the
    /// default once stateless lands in the runner.
    pub const fn current() -> Self {
        ProtocolVersion::V2025_06_18
    }

    /// True when the version uses the stateless transport (,
    /// SEP-2575): no `initialize` handshake, no `Mcp-Session-Id`,
    /// per-request `_meta` carries protocol version + client info, and
    /// `server/discover` returns capabilities on demand.
    pub const fn is_stateless(self) -> bool {
        matches!(self, ProtocolVersion::V2026_07_28)
    }
}

/// JSON-RPC ID. The MCP spec allows either an integer or a string; we
/// always allocate integers in this engine but parse both forms so
/// server-initiated requests stay round-trippable.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonRpcId {
    /// Numeric ID (the only form this engine allocates).
    Number(i64),
    /// String ID, sometimes used by other JSON-RPC peers.
    String(String),
}

impl JsonRpcId {
    /// Return the numeric form, if any. Helper for error reporting where
    /// callers want one canonical type to log.
    pub fn as_i64(&self) -> Option<i64> {
        match self {
            JsonRpcId::Number(n) => Some(*n),
            JsonRpcId::String(_) => None,
        }
    }
}

/// JSON-RPC envelope used on the wire.
///
/// The four canonical shapes (request, response, error, notification)
/// share enough fields that we keep them in a single struct and reject
/// invalid combinations at parse time via [`JsonRpcMessage::kind`].
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcMessage {
    /// Always `"2.0"` on the wire. Validated by [`JsonRpcMessage::kind`].
    pub jsonrpc: String,
    /// Present on requests and responses. Notifications omit the ID.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<JsonRpcId>,
    /// Present on requests and notifications.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub method: Option<String>,
    /// Present on requests and notifications.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
    /// Present on success responses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    /// Present on error responses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<JsonRpcErrorBody>,
}

/// JSON-RPC error body. Mirrors the wire shape (`code`, `message`,
/// optional `data`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcErrorBody {
    /// Numeric error code per the JSON-RPC spec.
    pub code: i64,
    /// Free-text human-readable message.
    pub message: String,
    /// Optional structured payload.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<serde_json::Value>,
}

/// Classified message kind, returned by [`JsonRpcMessage::kind`].
///
/// Routing decisions in the Client read this enum so the envelope's
/// per-field validation stays in one place.
#[derive(Debug, Clone, PartialEq)]
pub enum MessageKind {
    /// Client- or server-originated request awaiting a response.
    Request {
        /// Numeric or string ID assigned by the requester.
        id: JsonRpcId,
        /// JSON-RPC method name.
        method: String,
    },
    /// Successful response to a previously-sent request.
    Response {
        /// ID echoed back from the original request.
        id: JsonRpcId,
        /// Result payload (opaque to this layer).
        result: serde_json::Value,
    },
    /// Error response to a previously-sent request.
    ErrorResponse {
        /// ID echoed back from the original request.
        id: JsonRpcId,
        /// Error body from the peer.
        error: JsonRpcErrorBody,
    },
    /// Fire-and-forget notification (no ID).
    Notification {
        /// JSON-RPC method name.
        method: String,
    },
}

impl JsonRpcMessage {
    /// Construct a request envelope.
    pub fn request(id: JsonRpcId, method: impl Into<String>, params: serde_json::Value) -> Self {
        Self {
            jsonrpc: "2.0".into(),
            id: Some(id),
            method: Some(method.into()),
            params: Some(params),
            result: None,
            error: None,
        }
    }

    /// Construct a notification envelope (no ID).
    pub fn notification(method: impl Into<String>, params: serde_json::Value) -> Self {
        Self {
            jsonrpc: "2.0".into(),
            id: None,
            method: Some(method.into()),
            params: Some(params),
            result: None,
            error: None,
        }
    }

    /// Construct a successful response envelope.
    pub fn response(id: JsonRpcId, result: serde_json::Value) -> Self {
        Self {
            jsonrpc: "2.0".into(),
            id: Some(id),
            method: None,
            params: None,
            result: Some(result),
            error: None,
        }
    }

    /// Construct an error response envelope.
    pub fn error_response(id: JsonRpcId, body: JsonRpcErrorBody) -> Self {
        Self {
            jsonrpc: "2.0".into(),
            id: Some(id),
            method: None,
            params: None,
            result: None,
            error: Some(body),
        }
    }

    /// Classify the envelope into one of the four canonical shapes.
    ///
    /// Returns [`ProtocolError::MalformedMessage`] when the envelope
    /// does not fit any shape (for example a message that carries both
    /// `result` and `error`, or a response with no ID).
    pub fn kind(&self) -> Result<MessageKind, ProtocolError> {
        if self.jsonrpc != "2.0" {
            return Err(ProtocolError::MalformedMessage(format!(
                "expected jsonrpc=\"2.0\", got {:?}",
                self.jsonrpc
            )));
        }
        if self.result.is_some() && self.error.is_some() {
            return Err(ProtocolError::MalformedMessage(
                "envelope carries both `result` and `error`".into(),
            ));
        }
        match (&self.id, &self.method, &self.result, &self.error) {
            (Some(id), Some(method), None, None) => Ok(MessageKind::Request {
                id: id.clone(),
                method: method.clone(),
            }),
            (Some(id), None, Some(result), None) => Ok(MessageKind::Response {
                id: id.clone(),
                result: result.clone(),
            }),
            (Some(id), None, None, Some(err)) => Ok(MessageKind::ErrorResponse {
                id: id.clone(),
                error: err.clone(),
            }),
            (None, Some(method), None, None) => Ok(MessageKind::Notification {
                method: method.clone(),
            }),
            _ => Err(ProtocolError::MalformedMessage(
                "envelope does not match request, response, error, or notification shape".into(),
            )),
        }
    }
}

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

    #[test]
    fn parses_each_supported_protocol_version() {
        for raw in [
            "2024-11-05",
            "2025-03-26",
            "2025-06-18",
            "2025-11-25",
            "2026-03-26",
            "2026-07-28",
        ] {
            assert!(ProtocolVersion::from_wire(raw).is_some(), "expected {raw}");
        }
    }

    #[test]
    fn rejects_unknown_protocol_version() {
        assert!(ProtocolVersion::from_wire("2099-12-31").is_none());
    }

    #[test]
    fn protocol_version_round_trip() {
        for v in [
            ProtocolVersion::V2024_11_05,
            ProtocolVersion::V2025_03_26,
            ProtocolVersion::V2025_06_18,
            ProtocolVersion::V2025_11_25,
            ProtocolVersion::V2026_03_26,
            ProtocolVersion::V2026_07_28,
        ] {
            assert_eq!(ProtocolVersion::from_wire(v.as_wire()), Some(v));
        }
    }

    #[test]
    fn is_stateless_isolates_to_the_2026_07_28_revision() {
        // Only the 2026-07-28 revision uses the stateless
        // transport. Every earlier revision keeps the session-based path.
        assert!(ProtocolVersion::V2026_07_28.is_stateless());
        for v in [
            ProtocolVersion::V2024_11_05,
            ProtocolVersion::V2025_03_26,
            ProtocolVersion::V2025_06_18,
            ProtocolVersion::V2025_11_25,
            ProtocolVersion::V2026_03_26,
        ] {
            assert!(!v.is_stateless(), "{} must not be stateless", v.as_wire());
        }
    }

    #[test]
    fn classifies_request_response_error_and_notification() {
        let req = JsonRpcMessage::request(JsonRpcId::Number(1), "tools/list", json!({}));
        assert!(matches!(req.kind().unwrap(), MessageKind::Request { .. }));

        let resp = JsonRpcMessage::response(JsonRpcId::Number(1), json!({"ok": true}));
        assert!(matches!(resp.kind().unwrap(), MessageKind::Response { .. }));

        let err = JsonRpcMessage::error_response(
            JsonRpcId::Number(1),
            JsonRpcErrorBody {
                code: -32601,
                message: "method not found".into(),
                data: None,
            },
        );
        assert!(matches!(
            err.kind().unwrap(),
            MessageKind::ErrorResponse { .. }
        ));

        let note = JsonRpcMessage::notification("initialized", json!({}));
        assert!(matches!(
            note.kind().unwrap(),
            MessageKind::Notification { .. }
        ));
    }

    #[test]
    fn rejects_envelope_with_both_result_and_error() {
        let m = JsonRpcMessage {
            jsonrpc: "2.0".into(),
            id: Some(JsonRpcId::Number(1)),
            method: None,
            params: None,
            result: Some(json!({})),
            error: Some(JsonRpcErrorBody {
                code: -32601,
                message: "x".into(),
                data: None,
            }),
        };
        assert!(matches!(m.kind(), Err(ProtocolError::MalformedMessage(_))));
    }

    #[test]
    fn rejects_envelope_with_wrong_jsonrpc_version() {
        let m = JsonRpcMessage {
            jsonrpc: "1.0".into(),
            id: Some(JsonRpcId::Number(1)),
            method: Some("ping".into()),
            params: None,
            result: None,
            error: None,
        };
        assert!(matches!(m.kind(), Err(ProtocolError::MalformedMessage(_))));
    }
}
