//! Wire-protocol types for the `/crdt-sync` WebSocket protocol.

use serde::{Deserialize, Serialize};

// ── Wire protocol types ─────────────────────────────────────────────

/// Extended auth handshake: hello sent by the connecting peer to initiate the
/// extended mutual-auth handshake.  The connecting peer generates a fresh 32-byte
/// random nonce and sends it here so the responding node can sign it.
#[derive(Serialize, Deserialize, Debug)]
pub(super) struct HelloMessage {
    pub(super) r#type: String, // "hello"
    pub(super) nonce: String,  // 32-byte random nonce, hex-encoded (64 hex chars)
}

/// Extended auth handshake: server-auth reply sent by the responding node.
/// Contains the responding node's pubkey and a signature over the versioned
/// challenge `"huskies-v1:{nonce}"` derived from the connecting peer's nonce.
#[derive(Serialize, Deserialize, Debug)]
pub(super) struct ServerAuthMessage {
    pub(super) r#type: String, // "server_auth"
    pub(super) pubkey_hex: String,
    pub(super) signature_hex: String,
}

/// Auth handshake: challenge sent by the listener to the connector.
#[derive(Serialize, Deserialize, Debug)]
pub(super) struct ChallengeMessage {
    pub(super) r#type: String,
    pub(super) nonce: String,
}

/// Auth handshake: auth reply sent by the connector to the listener.
#[derive(Serialize, Deserialize, Debug)]
pub(super) struct AuthMessage {
    pub(super) r#type: String,
    pub(super) pubkey_hex: String,
    pub(super) signature_hex: String,
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum SyncMessage {
    /// Bulk state dump sent on connect (v1) or delta ops after clock exchange (v2).
    Bulk { ops: Vec<String> },
    /// A single new op.
    Op { op: String },
    /// Vector clock exchanged on connect (v2 protocol).
    ///
    /// Each entry maps a node's hex-encoded Ed25519 public key to the count of
    /// ops received from that node.  The receiving side computes the delta via
    /// [`crdt_state::ops_since`] and sends only the missing ops.
    Clock {
        clock: std::collections::HashMap<String, u64>,
    },
    /// Signals that the bulk-delta phase is complete; the sender is ready for
    /// real-time op streaming.  Locally-generated ops are buffered until the
    /// peer's `Ready` is received, then flushed in-order.
    Ready,
}

// ── RPC frame types ─────────────────────────────────────────────────

/// Multiplexed RPC frames sent over the `/crdt-sync` WebSocket alongside CRDT
/// sync messages.  The discriminator field is `"kind"` (not `"type"`) so that
/// these frames are unambiguously distinguishable from [`SyncMessage`] frames.
///
/// The caller is responsible for synthesising a `TIMEOUT` response locally if
/// its `ttl_ms` elapses with no `rpc_response` arriving — the server only
/// produces `NOT_FOUND` for unknown methods, not timeout frames.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum RpcFrame {
    /// Outbound RPC call from a peer.
    RpcRequest {
        /// Protocol version (currently `1`).
        version: u32,
        /// Caller-generated opaque ID used to correlate the response.
        correlation_id: String,
        /// Caller's deadline in milliseconds.  The server processes the call
        /// regardless; the caller synthesises a local `TIMEOUT` if it expires.
        ttl_ms: u64,
        /// Dot-separated method name, e.g. `"health.check"`.
        method: String,
        /// Arbitrary JSON parameters passed to the method handler.
        params: serde_json::Value,
    },
    /// Response sent back to the caller.
    RpcResponse {
        /// Protocol version (mirrors the request version).
        version: u32,
        /// Matches the `correlation_id` of the originating `rpc_request`.
        correlation_id: String,
        /// `true` if the method executed successfully.
        ok: bool,
        /// Present on success (`ok == true`).
        #[serde(skip_serializing_if = "Option::is_none")]
        result: Option<serde_json::Value>,
        /// Human-readable error description (present on `ok == false`).
        #[serde(skip_serializing_if = "Option::is_none")]
        error: Option<String>,
        /// Machine-readable error code (e.g. `"NOT_FOUND"`).  Present on
        /// `ok == false`.
        #[serde(skip_serializing_if = "Option::is_none")]
        code: Option<String>,
    },
}

/// Crate-visible re-export of `SyncMessage` for backwards-compatibility testing.
///
/// Used by `crdt_snapshot` tests to verify that snapshot messages are NOT
/// parseable as legacy `SyncMessage` variants — confirming that old peers
/// will gracefully reject them.
#[cfg(test)]
pub(crate) type SyncMessagePublic = SyncMessage;

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

    #[test]
    fn sync_message_bulk_serialization_roundtrip() {
        let msg = SyncMessage::Bulk {
            ops: vec!["op1".to_string(), "op2".to_string()],
        };
        let json = serde_json::to_string(&msg).unwrap();
        assert!(json.contains(r#""type":"bulk""#));
        let deserialized: SyncMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            SyncMessage::Bulk { ops } => {
                assert_eq!(ops.len(), 2);
                assert_eq!(ops[0], "op1");
                assert_eq!(ops[1], "op2");
            }
            _ => panic!("Expected Bulk"),
        }
    }

    #[test]
    fn sync_message_op_serialization_roundtrip() {
        let msg = SyncMessage::Op {
            op: r#"{"inner":{}}"#.to_string(),
        };
        let json = serde_json::to_string(&msg).unwrap();
        assert!(json.contains(r#""type":"op""#));
        let deserialized: SyncMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            SyncMessage::Op { op } => {
                assert_eq!(op, r#"{"inner":{}}"#);
            }
            _ => panic!("Expected Op"),
        }
    }

    #[test]
    fn sync_message_bulk_empty_ops() {
        let msg = SyncMessage::Bulk { ops: vec![] };
        let json = serde_json::to_string(&msg).unwrap();
        let deserialized: SyncMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            SyncMessage::Bulk { ops } => assert!(ops.is_empty()),
            _ => panic!("Expected Bulk"),
        }
    }

    #[test]
    fn sync_message_clock_serialization_roundtrip() {
        let mut clock = std::collections::HashMap::new();
        clock.insert("aabbcc00".to_string(), 42u64);
        clock.insert("ddeeff11".to_string(), 7u64);

        let msg = SyncMessage::Clock { clock };
        let json = serde_json::to_string(&msg).unwrap();
        assert!(json.contains(r#""type":"clock""#));

        let deserialized: SyncMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            SyncMessage::Clock { clock } => {
                assert_eq!(clock["aabbcc00"], 42);
                assert_eq!(clock["ddeeff11"], 7);
            }
            _ => panic!("Expected Clock"),
        }
    }

    #[test]
    fn sync_message_clock_empty() {
        let msg = SyncMessage::Clock {
            clock: std::collections::HashMap::new(),
        };
        let json = serde_json::to_string(&msg).unwrap();
        let deserialized: SyncMessage = serde_json::from_str(&json).unwrap();
        match deserialized {
            SyncMessage::Clock { clock } => assert!(clock.is_empty()),
            _ => panic!("Expected Clock"),
        }
    }

    #[test]
    fn sync_message_ready_serialization_roundtrip() {
        let msg = SyncMessage::Ready;
        let json = serde_json::to_string(&msg).unwrap();
        assert_eq!(json, r#"{"type":"ready"}"#);
        let deserialized: SyncMessage = serde_json::from_str(&json).unwrap();
        assert!(matches!(deserialized, SyncMessage::Ready));
    }

    #[test]
    fn rpc_request_serialization_roundtrip() {
        let frame = RpcFrame::RpcRequest {
            version: 1,
            correlation_id: "abc-123".to_string(),
            ttl_ms: 5000,
            method: "health.check".to_string(),
            params: serde_json::json!({}),
        };
        let json = serde_json::to_string(&frame).unwrap();
        assert!(
            json.contains(r#""kind":"rpc_request""#),
            "kind tag must be rpc_request"
        );
        assert!(json.contains("abc-123"));
        assert!(json.contains("health.check"));
        let back: RpcFrame = serde_json::from_str(&json).unwrap();
        match back {
            RpcFrame::RpcRequest {
                version,
                correlation_id,
                ttl_ms,
                method,
                ..
            } => {
                assert_eq!(version, 1);
                assert_eq!(correlation_id, "abc-123");
                assert_eq!(ttl_ms, 5000);
                assert_eq!(method, "health.check");
            }
            _ => panic!("Expected RpcRequest"),
        }
    }

    #[test]
    fn rpc_response_ok_serialization_roundtrip() {
        let frame = RpcFrame::RpcResponse {
            version: 1,
            correlation_id: "abc-123".to_string(),
            ok: true,
            result: Some(serde_json::json!({"status": "ok"})),
            error: None,
            code: None,
        };
        let json = serde_json::to_string(&frame).unwrap();
        assert!(json.contains(r#""kind":"rpc_response""#));
        assert!(json.contains(r#""ok":true"#));
        // error and code must be absent
        assert!(
            !json.contains("error"),
            "error field must be absent on success"
        );
        assert!(
            !json.contains("\"code\""),
            "code field must be absent on success"
        );
        let back: RpcFrame = serde_json::from_str(&json).unwrap();
        match back {
            RpcFrame::RpcResponse {
                ok,
                result,
                error,
                code,
                ..
            } => {
                assert!(ok);
                assert!(result.is_some());
                assert!(error.is_none());
                assert!(code.is_none());
            }
            _ => panic!("Expected RpcResponse"),
        }
    }

    #[test]
    fn rpc_response_not_found_serialization_roundtrip() {
        let frame = RpcFrame::RpcResponse {
            version: 1,
            correlation_id: "xyz-999".to_string(),
            ok: false,
            result: None,
            error: Some("method not found".to_string()),
            code: Some("NOT_FOUND".to_string()),
        };
        let json = serde_json::to_string(&frame).unwrap();
        assert!(json.contains(r#""ok":false"#));
        assert!(json.contains("NOT_FOUND"));
        assert!(
            !json.contains("\"result\""),
            "result must be absent on error"
        );
        let back: RpcFrame = serde_json::from_str(&json).unwrap();
        match back {
            RpcFrame::RpcResponse {
                ok, result, code, ..
            } => {
                assert!(!ok);
                assert!(result.is_none());
                assert_eq!(code.unwrap(), "NOT_FOUND");
            }
            _ => panic!("Expected RpcResponse"),
        }
    }

    #[test]
    fn rpc_request_not_parseable_as_sync_message() {
        let json = r#"{"kind":"rpc_request","version":1,"correlation_id":"x","ttl_ms":1000,"method":"health.check","params":{}}"#;
        let result: Result<SyncMessage, _> = serde_json::from_str(json);
        assert!(result.is_err(), "rpc_request must not parse as SyncMessage");
    }

    #[test]
    fn sync_message_not_parseable_as_rpc_frame() {
        let json = r#"{"type":"bulk","ops":[]}"#;
        let result: Result<RpcFrame, _> = serde_json::from_str(json);
        assert!(result.is_err(), "SyncMessage must not parse as RpcFrame");
    }
}
