use std::cmp::Ordering;

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

use crate::presence::ConnectionType;
#[cfg(doc)]
use crate::transport::Presence;

// NOTE(nimo): mod needed because `SiteId` is deprecated and gets used in serde derived impls
#[allow(deprecated)]
mod ditto_address {
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
    /// DittoPeer identifier
    #[serde(rename_all = "camelCase")]
    pub struct DittoAddress {
        pub(crate) pubkey: Vec<u8>,
        /// SiteId is deprecated, only included here to prevent serialization faults
        #[deprecated]
        #[doc(hidden)]
        #[serde(default)]
        pub(crate) site_id: Option<u64>,
    }
}

/// Graph of all known peers generated by [`Presence`] methods.
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PresenceGraph {
    /// The local peer.
    pub local_peer: Peer,
    /// Note that the peers in this set might not be directly connected to
    /// the local peer. Some peers might be be connected indirectly via a
    /// another peer or even form part of an isolated graph.
    ///
    /// Others yet might be discovered but disconnected due to version
    /// incompatibilities.
    pub remote_peers: Vec<Peer>,
}

/// Peer in the Ditto mesh.
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Peer {
    /// An address uniquely representing another device on the Ditto network.
    pub address: ditto_address::DittoAddress,

    /// The peer key is a unique identifier for a given peer, equal to or
    /// derived from the cryptographic public key used to authenticate it.
    ///
    /// NOTE: This will be empty when a peer is not updated to the latest
    /// version of the SDK.
    #[serde(rename = "peerKeyString")]
    pub peer_key: String,

    /// The human-readable device name for a peer. This defaults to the
    /// the hostname but can be manually set by the application developer.
    pub device_name: String,

    /// The operating system of a peer (if known).
    pub os: Option<PresenceOs>,

    /// Flag which indicates if this peer is connected to HyDRA. This is
    /// represented as a simple flag since attempting add HyDRA as a node
    /// to a graph would be extremely convoluted. The presence viewer
    /// depicts the HyDRA connection with a simple cloud icon above a peer.
    #[serde(alias = "isConnectedToDittoCloud")]
    pub is_connected_to_ditto_server: bool,

    /// A simplified boolean flag indicating whether the is peer is
    /// compatible with our own peer (if known). Note that there _might_
    /// be connections to this peer even if incompatible with our own
    /// peer, provided that some other peers are able to interoperate.
    pub is_compatible: Option<bool>,

    /// The marketing version of the SDK (if known). For instance: `"1.0.3"`.
    pub ditto_sdk_version: Option<String>,

    /// List of current connections between this peer and other peers.
    pub connections: Vec<Connection>,

    /// Metadata associated with the peer, empty dictionary by default.
    ///
    /// Use [`Presence::set_peer_metadata()`] or
    /// [`Presence::set_peer_metadata_json_str()`] to set this value.
    /// Peer metadata is dynamic and may change over the lifecycle of the
    /// [`PresenceGraph`]. It may be empty when a remote peer initially
    /// appears in the presence graph and will be updated once the peer has
    /// synced its metadata with the local peer.
    ///
    /// - See also: [`Presence::peer_metadata`] for details on usage of metadata.
    pub peer_metadata: JsonValue,

    /// Metadata associated with the peer by the identity service.
    ///
    /// Use an authentication webhook to set this value. See Ditto's online
    /// documentation for more information on how to configure an authentication
    /// webhook.
    pub identity_service_metadata: JsonValue,
}

impl ::core::fmt::Debug for Peer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Peer")
            .field("address", &self.address)
            .field("peer_key", &self.peer_key)
            .field("device_name", &self.device_name)
            .field("os", &self.os)
            .field(
                "is_connected_to_ditto_server",
                &self.is_connected_to_ditto_server,
            )
            .field("is_compatible", &self.is_compatible)
            .field("ditto_sdk_version", &self.ditto_sdk_version)
            .field("connections", &self.connections)
            .field("peer_metadata", &self.peer_metadata)
            .field("identity_service_metadata", &self.identity_service_metadata)
            .finish_non_exhaustive()
    }
}

/// Represents a connection between two peers in a Ditto mesh network.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Connection {
    /// An string ID in the form `"from<->to:connection_type"`. These ids
    /// are stable: the lower site Id will always be placed first.
    ///
    /// ## Example
    /// "1<->2:Bluetooth"
    pub id: String,

    /// The peer key of the peer at one end of the connection.
    ///
    /// The assignment to `peer1` and `peer2` is deterministic and stable for
    /// any two peers.
    #[serde(rename = "peerKeyString1")]
    pub peer1: String,

    /// The peer key of the peer at the other end of the connection.
    ///
    /// The assignment to `peer1` and `peer2` is deterministic and stable for
    /// any two peers.
    #[serde(rename = "peerKeyString2")]
    pub peer2: String,

    /// The type of connection. One `Connection` will exist
    /// for each connection type that might be active.
    pub connection_type: ConnectionType,
}

impl ::core::fmt::Debug for Connection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Connection")
            .field("id", &self.id)
            .field("peer1", &self.peer1)
            .field("peer2", &self.peer2)
            .field("connection_type", &self.connection_type)
            .finish_non_exhaustive()
    }
}

impl Eq for Connection {}
impl PartialEq<Self> for Connection {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl PartialOrd for Connection {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Connection {
    fn cmp(&self, other: &Self) -> Ordering {
        self.id.cmp(&other.id)
    }
}

/// Operating system used by a given peer.
#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
pub enum PresenceOs {
    /// Peer is a generic device with unknown OS.
    #[serde(rename = "Generic")]
    Generic,

    /// Peer is an Apple mobile device running iOS.
    #[serde(rename = "iOS")]
    Ios,

    /// Peer is an Android mobile device.
    #[serde(rename = "Android")]
    Android,

    /// Peer is a Linux device.
    #[serde(rename = "Linux")]
    Linux,

    /// Peer is a Windows device.
    #[serde(rename = "Windows")]
    Windows,

    /// Peer is an Apple device running MacOS.
    #[serde(rename = "macOS")]
    MacOS,
}

impl std::fmt::Display for PresenceOs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PresenceOs::Generic => write!(f, "Generic"),
            PresenceOs::Ios => write!(f, "iOS"),
            PresenceOs::Android => write!(f, "Android"),
            PresenceOs::Linux => write!(f, "Linux"),
            PresenceOs::MacOS => write!(f, "macOS"),
            PresenceOs::Windows => write!(f, "Windows"),
        }
    }
}

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

    const V3_PEER_JSON: &str = r#"
            {
                "localPeer": {
                    "address": {"pubkey":[1]},
                    "deviceName": "local-peer",
                    "peerKeyString": "pkAQ",
                    "peerMetadata": {},
                    "identityServiceMetadata": {},
                    "os": "macOS",
                    "isConnectedToDittoCloud": false,
                    "isCompatible": true,
                    "dittoSdkVersion": "1.0.0",
                    "connections" : [
                        {
                            "id": "pkAQ<->pkAg:Bluetooth",
                            "peerKeyString1": "pkAQ",
                            "peerKeyString2": "pkAg",
                            "connectionType": "Bluetooth"
                        },
                        {
                            "id": "pkAQ<->pkAw:AccessPoint",
                            "peerKeyString1": "pkAQ",
                            "peerKeyString2": "pkAw",
                            "connectionType": "AccessPoint"
                        },
                        {
                            "id": "pkAQ<->pkBA:WebSocket",
                            "peerKeyString1": "pkAQ",
                            "peerKeyString2": "pkBA",
                            "connectionType": "WebSocket"
                        }
                    ]
                },
                "remotePeers": [
                    {
                        "address": {"pubkey":[2]},
                        "peerKeyString": "pkAG",
                        "peerMetadata": {},
                        "identityServiceMetadata": {},
                        "deviceName": "device-2",
                        "os": "iOS",
                        "isConnectedToDittoCloud": false,
                        "isCompatible": true,
                        "dittoSdkVersion": null,
                        "connections" : [
                            {
                                "id": "pkAQ<->pkAg:Bluetooth",
                                "peerKeyString1": "pkAQ",
                                "peerKeyString2": "pkAg",
                                "connectionType": "Bluetooth"
                            }
                        ]
                    },
                    {
                        "address": {"pubkey":[3]},
                        "peerKeyString": "pkAw",
                        "peerMetadata": {},
                        "identityServiceMetadata": {},
                        "deviceName": "device-3",
                        "os": "Android",
                        "isConnectedToDittoCloud": false,
                        "isCompatible": true,
                        "dittoSdkVersion": "1.0.3",
                        "connections" : [
                            {
                                "id": "pkAQ<->pkAw:AccessPoint",
                                "peerKeyString1": "pkAQ",
                                "peerKeyString2": "pkAw",
                                "connectionType": "AccessPoint"
                            }
                        ]
                    },
                    {
                        "address": {"pubkey":[4]},
                        "peerKeyString": "pkBA",
                        "peerMetadata": {},
                        "identityServiceMetadata": {},
                        "deviceName": "device-4",
                        "os": "Linux",
                        "isConnectedToDittoCloud": true,
                        "isCompatible": true,
                        "dittoSdkVersion": null,
                        "connections" : [
                            {
                                "id": "pkAQ<->pkBA:WebSocket",
                                "peerKeyString1": "pkAQ",
                                "peerKeyString2": "pkBA",
                                "connectionType": "WebSocket"
                            }
                        ]
                    }
                ]
            }
        "#;

    #[test]
    fn test_json_parsing() {
        #![allow(deprecated)]

        let graph: PresenceGraph = serde_json::from_str(V3_PEER_JSON).unwrap();

        // Local Peer
        assert_eq!(graph.local_peer.address.pubkey, vec![1]);
        assert_eq!(graph.local_peer.device_name, "local-peer");
        assert_eq!(graph.local_peer.os, Some(PresenceOs::MacOS));
        assert!(!graph.local_peer.is_connected_to_ditto_server);
        assert_eq!(graph.local_peer.connections.len(), 3);
        assert_eq!(graph.local_peer.peer_key, "pkAQ");
        assert_eq!(graph.local_peer.connections[0].peer1, "pkAQ");
        assert_eq!(graph.local_peer.connections[0].peer2, "pkAg");
        assert_eq!(graph.local_peer.connections[1].peer1, "pkAQ");
        assert_eq!(graph.local_peer.connections[1].peer2, "pkAw");
        assert_eq!(graph.local_peer.connections[2].peer1, "pkAQ");
        assert_eq!(graph.local_peer.connections[2].peer2, "pkBA");

        // Remote Peers

        assert_eq!(graph.remote_peers[0].address.pubkey, vec![2]);
        assert_eq!(graph.remote_peers[0].device_name, "device-2");
        assert_eq!(graph.remote_peers[0].os, Some(PresenceOs::Ios));
        assert!(!graph.remote_peers[0].is_connected_to_ditto_server);
        let connections = &graph.remote_peers[0].connections;
        assert_eq!(connections[0].peer1, "pkAQ");
        assert_eq!(connections[0].peer2, "pkAg");

        assert_eq!(graph.remote_peers[1].address.pubkey, vec![3]);
        assert_eq!(graph.remote_peers[1].device_name, "device-3");
        assert_eq!(graph.remote_peers[1].os, Some(PresenceOs::Android));
        assert!(!graph.remote_peers[1].is_connected_to_ditto_server);
        let connections = &graph.remote_peers[1].connections;
        assert_eq!(connections[0].peer1, "pkAQ");
        assert_eq!(connections[0].peer2, "pkAw");

        assert_eq!(graph.remote_peers[2].address.pubkey, vec![4]);
        assert_eq!(graph.remote_peers[2].device_name, "device-4");
        assert_eq!(graph.remote_peers[2].os, Some(PresenceOs::Linux));
        assert!(graph.remote_peers[2].is_connected_to_ditto_server);
        let connections = &graph.remote_peers[2].connections;
        assert_eq!(connections[0].peer1, "pkAQ");
        assert_eq!(connections[0].peer2, "pkBA");
    }
}
