//! JSON-RPC 2.0 envelope + A2A method enum + standard A2A headers.
//!
//! Wire format mirrors the A2A v1.0 spec §9 ("JSON-RPC Surface"). See
//! <https://a2a-protocol.org/latest/specification/>.

use crate::error::A2aError;
use serde::{Deserialize, Serialize};

/// JSON-RPC 2.0 error codes used across A2A transports.
///
/// Centralised here so the NATS server (`server.rs`), HTTP transport
/// (`http.rs`), and error-mapping impl (`error.rs`) all read from one
/// source of truth rather than carrying local integer literals.
pub mod codes {
    /// JSON-RPC 2.0 -32700: parse error.
    pub const PARSE_ERROR: i32 = -32700;
    /// JSON-RPC 2.0 -32600: invalid request.
    pub const INVALID_REQUEST: i32 = -32600;
    /// JSON-RPC 2.0 -32601: method not found.
    pub const METHOD_NOT_FOUND: i32 = -32601;
    /// JSON-RPC 2.0 -32602: invalid params.
    pub const INVALID_PARAMS: i32 = -32602;
    /// Server-defined -32000: generic internal error.
    pub const SERVER_ERROR: i32 = -32000;
    /// klieo-a2a -32001: authentication required.
    pub const UNAUTHENTICATED: i32 = -32001;
    /// Klieo-specific: resume requested with a cursor older than the
    /// oldest retained event in the buffer.
    pub const RESUME_BUFFER_EXPIRED: i32 = -32010;
    /// Stream leader (originating replica running the invoke) died
    /// or never claimed leadership. Follower observed an orphaned
    /// resume buffer + wrote a terminal frame so the client sees
    /// clean termination + can retry. ADR-020.
    pub const LEADER_DIED: i32 = -32099;
}

/// JSON-RPC 2.0 request envelope.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
    /// Always `"2.0"`.
    pub jsonrpc: String,
    /// Request id (number, string, or null).
    pub id: serde_json::Value,
    /// One of the 11 [`A2aMethod`] variants.
    pub method: String,
    /// Method-specific params payload.
    #[serde(default)]
    pub params: serde_json::Value,
}

/// JSON-RPC 2.0 response envelope.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
    /// Always `"2.0"`.
    pub jsonrpc: String,
    /// Mirrors the request id.
    pub id: serde_json::Value,
    /// Successful result (mutually exclusive with `error`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    /// Error payload (mutually exclusive with `result`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<JsonRpcError>,
}

/// JSON-RPC 2.0 error payload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
    /// Numeric code (negative for server errors).
    pub code: i32,
    /// Human-readable message.
    pub message: String,
    /// Optional structured detail.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<serde_json::Value>,
}

/// The 11 method names from A2A v1.0 spec §9.4.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum A2aMethod {
    /// §9.4.1.
    SendMessage,
    /// §9.4.2.
    SendStreamingMessage,
    /// §9.4.3.
    GetTask,
    /// §9.4.4.
    ListTasks,
    /// §9.4.5.
    CancelTask,
    /// §9.4.6.
    SubscribeToTask,
    /// §9.4.7.
    CreateTaskPushNotificationConfig,
    /// §9.4.8.
    GetTaskPushNotificationConfig,
    /// §9.4.9.
    ListTaskPushNotificationConfigs,
    /// §9.4.10.
    DeleteTaskPushNotificationConfig,
    /// §9.4.11.
    GetExtendedAgentCard,
}

impl A2aMethod {
    /// Parse a JSON-RPC `method` string. Unknown method names return
    /// [`A2aError::MethodNotFound`].
    #[allow(clippy::should_implement_trait)] // Inherent fn matches the plan; trait `FromStr` would change error type.
    pub fn from_str(s: &str) -> Result<Self, A2aError> {
        Ok(match s {
            "SendMessage" => Self::SendMessage,
            "SendStreamingMessage" => Self::SendStreamingMessage,
            "GetTask" => Self::GetTask,
            "ListTasks" => Self::ListTasks,
            "CancelTask" => Self::CancelTask,
            "SubscribeToTask" => Self::SubscribeToTask,
            "CreateTaskPushNotificationConfig" => Self::CreateTaskPushNotificationConfig,
            "GetTaskPushNotificationConfig" => Self::GetTaskPushNotificationConfig,
            "ListTaskPushNotificationConfigs" => Self::ListTaskPushNotificationConfigs,
            "DeleteTaskPushNotificationConfig" => Self::DeleteTaskPushNotificationConfig,
            "GetExtendedAgentCard" => Self::GetExtendedAgentCard,
            other => return Err(A2aError::MethodNotFound(other.to_string())),
        })
    }
}

/// Standard A2A headers extracted from a transport-layer header bag.
///
/// Header names match A2A v1.0 spec §8 ("Headers"). Defaults for
/// missing headers: `a2a_version = "1.0"`, everything else `None` /
/// empty.
#[derive(Debug, Clone)]
pub struct A2aHeaders {
    /// `Authorization` header (full value, including scheme).
    pub authorization: Option<String>,
    /// `Content-Type` header.
    pub content_type: Option<String>,
    /// `A2A-Version` — defaults to `"1.0"` when absent.
    pub a2a_version: String,
    /// `A2A-Extensions` — comma-split, trimmed.
    pub a2a_extensions: Vec<String>,
}

impl klieo_auth_common::Headers for A2aHeaders {
    /// Case-insensitive lookup over the four decoded A2A header slots.
    /// Returns `None` for header names outside the known A2A set —
    /// authenticators that need a richer header surface should keep
    /// hold of the raw [`klieo_core::Headers`] bag.
    fn get(&self, name: &str) -> Option<&str> {
        match name.to_ascii_lowercase().as_str() {
            "authorization" => self.authorization.as_deref(),
            "content-type" => self.content_type.as_deref(),
            "a2a-version" => Some(self.a2a_version.as_str()),
            _ => None,
        }
    }
}

impl A2aHeaders {
    /// Decode the four standard A2A headers from a [`klieo_core::Headers`]
    /// map. Missing headers fall back to defaults.
    ///
    /// HTTP header names are case-insensitive (RFC 7230 §3.2). The
    /// upstream [`klieo_core::Headers`] is a plain `HashMap<String,
    /// String>` so we perform the case-fold lookup here — axum's
    /// `HeaderMap` iterator emits lowercase names, while NATS callers
    /// historically use title case (`Authorization`).
    pub fn decode_from(headers: &klieo_core::Headers) -> Self {
        let get = |k: &str| {
            headers
                .iter()
                .find_map(|(name, value)| name.eq_ignore_ascii_case(k).then(|| value.clone()))
        };
        Self {
            authorization: get("Authorization"),
            content_type: get("Content-Type"),
            a2a_version: get("A2A-Version").unwrap_or_else(|| "1.0".to_string()),
            a2a_extensions: get("A2A-Extensions")
                .map(|v| {
                    v.split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect()
                })
                .unwrap_or_default(),
        }
    }
}

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

    #[test]
    fn envelope_request_round_trips() {
        let req = JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id: json!(42),
            method: "SendMessage".into(),
            params: json!({"message": {"messageId": "m-1", "role": "user", "parts": []}}),
        };
        let v = serde_json::to_value(&req).unwrap();
        let back: JsonRpcRequest = serde_json::from_value(v).unwrap();
        assert_eq!(back.method, "SendMessage");
        assert_eq!(back.id, json!(42));
    }

    #[test]
    fn method_send_message_parses() {
        let m = A2aMethod::from_str("SendMessage").unwrap();
        assert_eq!(m, A2aMethod::SendMessage);
    }

    #[test]
    fn unknown_method_returns_method_not_found() {
        let err = A2aMethod::from_str("Frobnicate").unwrap_err();
        assert!(matches!(err, crate::error::A2aError::MethodNotFound(_)));
    }

    #[test]
    fn headers_decode_with_all_four_fields() {
        let mut h = klieo_core::Headers::new();
        h.insert("Authorization".into(), "Bearer abc".into());
        h.insert("Content-Type".into(), "application/json".into());
        h.insert("A2A-Version".into(), "1.0".into());
        h.insert("A2A-Extensions".into(), "ext1,ext2".into());
        let decoded = A2aHeaders::decode_from(&h);
        assert_eq!(decoded.authorization.as_deref(), Some("Bearer abc"));
        assert_eq!(decoded.a2a_version, "1.0");
        assert_eq!(
            decoded.a2a_extensions,
            vec!["ext1".to_string(), "ext2".to_string()]
        );
    }

    #[test]
    fn missing_version_defaults_to_one_zero() {
        let h = klieo_core::Headers::new();
        let decoded = A2aHeaders::decode_from(&h);
        assert_eq!(decoded.a2a_version, "1.0");
    }
}
