//! Apollo subscription callback protocol support
//!
//! This module provides types and helpers for the [Apollo Callback Protocol](https://www.apollographql.com/docs/router/executing-operations/subscription-callback-protocol/)
//! for GraphQL subscriptions over HTTP.
//!
//! The callback protocol allows subscriptions to work over HTTP by having the
//! server POST subscription events to a client-provided callback URL.
//!
//! # Protocol Overview
//!
//! 1. Client sends a subscription request via HTTP POST with a callback URL
//! 2. Server responds with a subscription ID and heartbeat/complete URLs
//! 3. Server sends subscription events to the client's callback URL via HTTP
//!    POST
//! 4. Client can send heartbeat (check) messages or complete the subscription

use serde::{Deserialize, Serialize};

use crate::{Request, Response, from_value};

/// The content type for callback protocol messages
pub const CALLBACK_CONTENT_TYPE: &str = "application/json";

/// Checkpoint header value for callback protocol
pub const CHECKPOINT_HEADER_VALUE: &str = "1";

/// Subscription callback payload sent by the client in the initial subscription
/// request
///
/// This is included in the `extensions` field of a GraphQL request to indicate
/// that the client wants to use the callback protocol.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallbackPayload {
    /// The URL where the server should POST subscription events
    pub callback_url: String,
    /// Optional subscription ID (generated by client or server)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subscription_id: Option<String>,
}

impl CallbackPayload {
    /// Create a new callback payload with the given callback URL
    pub fn new(callback_url: impl Into<String>) -> Self {
        Self {
            callback_url: callback_url.into(),
            subscription_id: None,
        }
    }

    /// Set the subscription ID
    #[must_use]
    pub fn with_subscription_id(mut self, id: impl Into<String>) -> Self {
        self.subscription_id = Some(id.into());
        self
    }

    /// Extract the callback payload from a GraphQL request's extensions
    pub fn from_request(request: &Request) -> Option<Self> {
        request
            .extensions
            .get("subscription")
            .and_then(|value| from_value(value.clone()).ok())
    }
}

/// Response sent by the server when accepting a callback subscription
///
/// This is returned in the initial HTTP response to the subscription request.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallbackResponse {
    /// The unique subscription ID for this subscription
    pub subscription_id: String,
    /// URLs for client to send heartbeat/complete messages
    pub urls: CallbackUrls,
}

/// URLs provided by the server for client-side subscription management
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallbackUrls {
    /// URL for sending heartbeat (check) messages to keep subscription alive
    pub check: String,
    /// URL for completing (terminating) the subscription
    #[serde(rename = "complete")]
    pub complete_url: String,
}

/// Events that the server sends to the client's callback URL
///
/// These events are POSTed to the client's callback URL as they occur.
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum CallbackEvent {
    /// A heartbeat/check message to verify the connection is alive
    Check {
        /// The subscription ID this event belongs to
        id: String,
        /// Timestamp of the heartbeat
        timestamp: u64,
    },
    /// Next subscription result
    Next {
        /// The subscription ID this event belongs to
        id: String,
        /// The GraphQL response payload
        payload: Response,
        /// Timestamp of the event
        timestamp: u64,
    },
    /// Subscription completed (either by server or client)
    Complete {
        /// The subscription ID this event belongs to
        id: String,
        /// Optional final response payload
        #[serde(skip_serializing_if = "Option::is_none")]
        payload: Option<Response>,
    },
}

/// Messages that the client sends to the server's check/complete URLs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum CallbackClientMessage {
    /// Heartbeat/check message from client
    Check {
        /// The subscription ID
        id: String,
        /// Optional timestamp
        #[serde(skip_serializing_if = "Option::is_none")]
        timestamp: Option<u64>,
    },
    /// Complete message from client to terminate subscription
    Complete {
        /// The subscription ID
        id: String,
    },
}

/// Generate a unique subscription ID
pub fn generate_subscription_id() -> String {
    use std::{
        sync::atomic::{AtomicU64, Ordering},
        time::{SystemTime, UNIX_EPOCH},
    };

    static NEXT_ID: AtomicU64 = AtomicU64::new(1);

    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);

    format!("sub_{timestamp}_{id:x}")
}

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

    #[test]
    fn test_callback_payload_serialization() {
        let payload =
            CallbackPayload::new("http://localhost:8080/callback").with_subscription_id("test-123");

        let json = serde_json::to_string(&payload).unwrap();
        assert!(json.contains("http://localhost:8080/callback"));
        assert!(json.contains("\"subscriptionId\":\"test-123\""));
        assert!(json.contains("\"callbackUrl\":"));

        let deserialized: CallbackPayload = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.callback_url, "http://localhost:8080/callback");
        assert_eq!(deserialized.subscription_id, Some("test-123".to_string()));
    }

    #[test]
    fn test_callback_response_serialization() {
        let response = CallbackResponse {
            subscription_id: "sub-123".to_string(),
            urls: CallbackUrls {
                check: "http://localhost:8080/check/sub-123".to_string(),
                complete_url: "http://localhost:8080/complete/sub-123".to_string(),
            },
        };

        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("\"subscriptionId\":\"sub-123\""));

        let deserialized: CallbackResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.subscription_id, "sub-123");
        assert_eq!(
            deserialized.urls.check,
            "http://localhost:8080/check/sub-123"
        );
    }

    #[test]
    fn test_callback_event_serialization() {
        let check_event = CallbackEvent::Check {
            id: "sub-123".to_string(),
            timestamp: 1234567890,
        };

        let json = serde_json::to_string(&check_event).unwrap();
        assert!(json.contains("\"kind\":\"check\""));
        assert!(json.contains("\"id\":\"sub-123\""));

        let next_event = CallbackEvent::Next {
            id: "sub-123".to_string(),
            payload: Response::new(value!({"value": 42})),
            timestamp: 1234567890,
        };

        let json = serde_json::to_string(&next_event).unwrap();
        assert!(json.contains("\"kind\":\"next\""));

        let complete_event = CallbackEvent::Complete {
            id: "sub-123".to_string(),
            payload: None,
        };

        let json = serde_json::to_string(&complete_event).unwrap();
        assert!(json.contains("\"kind\":\"complete\""));
    }

    #[test]
    fn test_callback_client_message_serialization() {
        let check_msg = CallbackClientMessage::Check {
            id: "sub-123".to_string(),
            timestamp: Some(1234567890),
        };

        let json = serde_json::to_string(&check_msg).unwrap();
        let deserialized: CallbackClientMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            CallbackClientMessage::Check { id, timestamp } => {
                assert_eq!(id, "sub-123");
                assert_eq!(timestamp, Some(1234567890));
            }
            CallbackClientMessage::Complete { .. } => panic!("Expected Check message"),
        }

        let complete_msg = CallbackClientMessage::Complete {
            id: "sub-123".to_string(),
        };

        let json = serde_json::to_string(&complete_msg).unwrap();
        let deserialized: CallbackClientMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            CallbackClientMessage::Complete { id } => {
                assert_eq!(id, "sub-123");
            }
            CallbackClientMessage::Check { .. } => panic!("Expected Complete message"),
        }
    }

    #[test]
    fn test_generate_subscription_id() {
        let id1 = generate_subscription_id();
        let id2 = generate_subscription_id();

        assert!(id1.starts_with("sub_"));
        assert!(id2.starts_with("sub_"));
        assert_ne!(id1, id2);
    }

    #[test]
    fn test_callback_payload_from_request() {
        let mut request = Request::new("subscription { onMessage { content } }");
        request.extensions.insert(
            "subscription".to_string(),
            value!({"callbackUrl": "http://client:3000/callback"}),
        );

        let payload = CallbackPayload::from_request(&request);
        assert!(payload.is_some());

        let payload = payload.unwrap();
        assert_eq!(payload.callback_url, "http://client:3000/callback");
    }

    #[test]
    fn test_callback_payload_from_request_missing() {
        let request = Request::new("subscription { onMessage { content } }");
        let payload = CallbackPayload::from_request(&request);
        assert!(payload.is_none());
    }
}
