use std::cmp::Ordering;

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

#[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>,
        pub(crate) site_id: Option<crate::transport::SiteId>,
    }
}
pub use self::ditto_address::DittoAddress;

/// 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: DittoAddress,

    #[doc(hidden)]
    #[deprecated(note = "use `.peer_key_string` instead")]
    pub peer_key: Vec<u8>,

    /// 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.
    pub peer_key_string: 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>,

    /// This field is no longer in use and will always be `0`.
    #[doc(hidden)]
    #[deprecated(
        note = "This feature has been removed, this value will always be 0 from SDK release 4.8.0"
    )]
    #[serde(default, skip_deserializing)]
    pub query_overlap_group: u8,

    /// 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.
    pub is_connected_to_ditto_cloud: 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_string", &self.peer_key_string)
            .field("device_name", &self.device_name)
            .field("os", &self.os)
            .field(
                "is_connected_to_ditto_cloud",
                &self.is_connected_to_ditto_cloud,
            )
            .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()
    }
}

// Re-exporting using the old name for this type, `UndirectedConnection`
//
// Technically this shouldn't be strictly necessary because `UndirectedConnection`
// was actually unnameable from outside the crate, so no client code should
// have been able to construct or destructure or even import this type by its
// old name anyway.
#[doc(hidden)]
#[allow(unused_imports)]
#[deprecated(note = "Use `Connection` instead")]
pub use Connection as UndirectedConnection;

/// 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 with the lower Peer Key.
    #[doc(hidden)]
    #[deprecated(note = "use `.peer_key_string1` instead")]
    pub peer1: Vec<u8>,

    /// The peer with the higher Peer Key.
    #[doc(hidden)]
    #[deprecated(note = "use `.peer_key_string2` instead")]
    pub peer2: Vec<u8>,

    /// The peer key of the peer at one end of the connection, as a String
    ///
    /// The assignment to `peer_key_string1` and `peer_key_string2` is deterministic
    /// and stable for any two peers.
    pub peer_key_string1: String,

    /// The peer key of the peer at the other end of the connection, as a String
    ///
    /// The assignment to `peer_key_string1` and `peer_key_string2` is deterministic
    /// and stable for any two peers.
    pub peer_key_string2: String,

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

    /// A made-up figure that changes based on RSSI. This may become meaningful
    /// if more accurate range-finding transports are added in future.
    pub approximate_distance_in_meters: Option<f32>,
}

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("peer_key_string1", &self.peer_key_string1)
            .field("peer_key_string2", &self.peer_key_string2)
            .field("connection_type", &self.connection_type)
            .field(
                "approximate_distance_in_meters",
                &self.approximate_distance_in_meters,
            )
            .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"),
        }
    }
}

// NOTE(nick): There are a few weird things going on here, allow me to explain:
//
// - Circa June 2024 we are deprecating this type in favor of [`super::ConnectionType`].
//
// - The deprecation note says "Use `ConnectionType` instead", because this type is only visible to
//   the outside via `dittolive_ditto::transport::V3ConnectionType` due to a re-export in the
//   transport module. Therefore the deprecation note is telling users to move from
//   `V3ConnectionType` to just `ConnectionType` in the same module
//
// - When adding the `#[deprecated]` tag, the derived impls of `Serialize` and `Deserialize` for
//   `ConnectionType` caused warnings (aka CI failures) because those impls referred to the type and
//   the fields, triggering the deprecation warning. Adding an `#[allow(deprecated)]` to the type
//   itself did not resolve the warnings for the derived impls.
//
// - To work around this, we put the type into its own module and used `#[allow(deprecated)]` on the
//   whole module, which encompasses all of the derived impls as well. Then we needed a simple
//   re-export which also needed `#[allow(deprecated)]`
#[allow(deprecated)]
pub use connection_type::ConnectionType;
#[allow(deprecated)]
mod connection_type {
    use super::*;

    /// Defines a simplified connection type between peers for reporting presence
    /// info.
    ///
    /// These connections indicate P2P connections _only_. A connection to HyDRA
    /// is recorded by a simple boolean flag on the peer.
    #[doc(hidden)]
    #[deprecated(note = "Use `ConnectionType` instead")]
    #[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy, Debug, Serialize, Deserialize)]
    pub enum ConnectionType {
        Bluetooth,
        AccessPoint,
        P2PWiFi,
        WebSocket,
    }
}

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

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

    #[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_eq!(graph.local_peer.query_overlap_group, 0);
        assert_eq!(graph.local_peer.connections.len(), 3);
        assert_eq!(graph.local_peer.peer_key, vec![1]);
        assert_eq!(graph.local_peer.peer_key_string, "pkAQ");
        assert_eq!(graph.local_peer.connections[0].peer_key_string1, "pkAQ");
        assert_eq!(graph.local_peer.connections[0].peer_key_string2, "pkAg");
        assert_eq!(graph.local_peer.connections[1].peer_key_string1, "pkAQ");
        assert_eq!(graph.local_peer.connections[1].peer_key_string2, "pkAw");
        assert_eq!(graph.local_peer.connections[2].peer_key_string1, "pkAQ");
        assert_eq!(graph.local_peer.connections[2].peer_key_string2, "pkBA");

        // Remote Peers

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

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

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

        #[allow(deprecated)]
        {
            assert_eq!(graph.local_peer.query_overlap_group, 0);
            assert_eq!(graph.remote_peers[0].query_overlap_group, 0);
            assert_eq!(graph.remote_peers[1].query_overlap_group, 0);
            assert_eq!(graph.remote_peers[2].query_overlap_group, 0);
        }
    }
}
