use std::{collections::HashSet, path::PathBuf, time::Duration};

use serde::{Deserialize, Serialize};
use serde_with::serde_as;

pub const NO_PREFERRED_ROUTE_HINT: u32 = 0;

/// A configuration object specifying which network transports Ditto should use
/// to sync data.
///
/// A `Ditto` object comes with a default transport configuration where all
/// available peer-to-peer transports are enabled. You can customize this by
/// initializing a `TransportConfig`, adjusting its properties, and supplying it
/// to `set_transport_config()` on `Ditto`.
///
/// When you initialize a `TransportConfig` yourself it starts with all
/// transports disabled. You must enable each one directly.
///
/// Peer-to-peer transports will automatically discover peers in the vicinity
/// and create connections without any configuration. These are configured
/// inside the `peer_to_peer` property. To turn each one on, set its `enabled`
/// property to `true`.
///
/// To connect to a peer at a known location, such as a Ditto Big Peer, add its
/// address inside the `connect` configuration. These are either `host:port`
/// strings for raw TCP sync, or a `wss://…` URL for websockets.
///
/// The `listen` configurations are for specific less common data sync
/// scenarios. Please read the documentation on the Ditto website for examples.
/// Incorrect use of `listen` can result in insecure configurations.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct TransportConfig {
    /// Configure Bluetooth and LAN transports.
    pub peer_to_peer: PeerToPeer,

    /// Configure TCP and WebSocket outbound transports.
    pub connect: Connect,

    /// Configure TCP and HTTP inbound transports.
    pub listen: Listen,

    /// Set peer-global transport settings.
    pub global: Global,
}

impl TransportConfig {
    /// Create a new [`TransportConfig`] with default settings.
    pub fn new() -> Self {
        Self {
            peer_to_peer: PeerToPeer {
                bluetooth_le: BluetoothLEConfig::new(),
                lan: LanConfig::new(),
                awdl: AwdlConfig::new(),
                wifi_aware: WifiAwareConfig::new(),
                #[cfg(feature = "__ditto_multicast_beta")]
                multicast: MulticastConfig::new(),
            },
            connect: Connect {
                tcp_servers: HashSet::new(),
                websocket_urls: HashSet::new(),
                retry_interval: Duration::from_secs(5),
            },
            listen: Listen {
                tcp: TcpListenConfig::new(),
                http: HttpListenConfig::new(),
            },
            global: Global {
                sync_group: 0,
                routing_hint: NO_PREFERRED_ROUTE_HINT,
            },
        }
    }

    /// Enables peer-to-peer transports in this config.
    pub fn enable_all_peer_to_peer(&mut self) {
        self.peer_to_peer.bluetooth_le.enabled = true;
        self.peer_to_peer.lan.enabled = true;
        self.peer_to_peer.awdl.enabled = true;
        self.peer_to_peer.wifi_aware.enabled = true;
    }
}

/// Configure peer-to-peer transports such as Bluetooth and LAN.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct PeerToPeer {
    /// Bluetooth transport configurations.
    pub bluetooth_le: BluetoothLEConfig,

    /// LAN transport configurations.
    pub lan: LanConfig,

    /// AWDL transport configurations.
    pub awdl: AwdlConfig,

    /// Wi-Fi Aware transport configurations.
    pub wifi_aware: WifiAwareConfig,

    /// Multicast data-sync transport. Distinct from
    /// [`LanConfig::multicast_enabled`] which is mDNS-multicast LAN
    /// discovery. Suitable for mesh / tactical-radio scenarios.
    #[cfg(feature = "__ditto_multicast_beta")]
    pub multicast: MulticastConfig,
}

/// Configure outbound transports such as TCP and WebSocket dialing.
#[serde_as]
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct Connect {
    /// A set of TCP servers to attempt connection to.
    pub tcp_servers: HashSet<String>,

    /// A set of websocket servers to attempt connection to.
    pub websocket_urls: HashSet<String>,

    /// The retry interval between failed connection attempts. For
    /// cross-compatibility, this must be less than 2^32 - 1 milliseconds.
    #[serde_as(as = "::serde_with::DurationMilliSeconds<u64>")]
    pub retry_interval: Duration,
}

/// Configure inbound transports such as TCP and HTTP servers.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct Listen {
    /// Configure inbound TCP transports.
    pub tcp: TcpListenConfig,

    /// Configure inbound HTTP transports.
    pub http: HttpListenConfig,
}

/// Settings not associated with any specific type of transport.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct Global {
    /// The sync group for this device.
    ///
    /// When peer-to-peer transports are enabled, all devices with the same App
    /// ID will normally form an interconnected mesh network. In some
    /// situations it may be desirable to have distinct groups of devices
    /// within the same app, so that connections will only be formed within
    /// each group. The `sync_group` parameter changes that group
    /// membership. A device can only ever be in one sync group, which
    /// by default is group 0. Up to 2^32 distinct group numbers can be used in
    /// an app.
    ///
    /// This is an optimization, not a security control. If a connection is
    /// created manually, such as by specifying a `connect` transport, then
    /// devices from different sync groups will still sync as normal. If
    /// two groups of devices are intended to have access to different data
    /// sets, this must be enforced using Ditto's permissions system.
    pub sync_group: u32,

    /// The routing hint for this device.
    ///
    /// A routing hint is a performance tuning option which can improve the performance of
    /// applications that use large collections. Ditto will make a best effort to co-locate data
    /// for the same routing key. In most circumstances, this should substantially improve
    /// responsiveness of the Ditto Cloud.
    ///
    /// The value of the routing hint is application specific - you are free to choose any value.
    /// Devices which you expect to operate on much the same data should be configured to
    /// use the same value.
    ///
    /// A routing hint does not partition data. The value of the routing hint will not affect the
    /// data returned for a query. The routing hint only improves the efficiency of the Cloud's
    /// ability to satisfy the query.
    pub routing_hint: u32,
}

/// Configure inbound HTTP transports.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct HttpListenConfig {
    /// Whether inbound HTTP is enabled.
    pub enabled: bool,

    /// The IP address on which to bind a listening HTTP server.
    pub interface_ip: String,

    /// The port on which to bind a listening HTTP server.
    pub port: u16,

    /// Whether to enable Websocket sync on this transport.
    pub websocket_sync: bool,

    /// Optional path to a TLS key file.
    pub tls_key_path: Option<PathBuf>,

    /// Optional path to a TLS certificate file.
    pub tls_certificate_path: Option<PathBuf>,
}

impl HttpListenConfig {
    /// Create a new [`HttpListenConfig`] with default settings.
    pub fn new() -> Self {
        Self {
            enabled: false,
            interface_ip: "[::]".to_string(),
            port: 80,
            websocket_sync: true,
            tls_key_path: None,
            tls_certificate_path: None,
        }
    }
}

/// Configure inbound TCP transports.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct TcpListenConfig {
    /// Whether inbound TCP is enabled.
    pub enabled: bool,

    /// The address to bind a TCP listener, such as `0.0.0.0`.
    pub interface_ip: String,

    /// The port to bind the TCP listener to.
    pub port: u16,
}

impl TcpListenConfig {
    /// Create a new [`TcpListenConfig`] with default settings.
    pub fn new() -> Self {
        Self {
            enabled: false,
            interface_ip: "[::]".to_string(),
            port: 4040,
        }
    }
}

/// Configure the Bluetooth Low-Energy transport.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct BluetoothLEConfig {
    /// Whether to enable the Bluetooth Low-Energy transport.
    pub enabled: bool,
}

impl BluetoothLEConfig {
    /// Create a new [`BluetoothLEConfig`] with default settings.
    pub fn new() -> Self {
        Self { enabled: false }
    }
}

/// Configure the Local Area Network (LAN) transport.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct LanConfig {
    /// Whether to enable the LAN transport.
    pub enabled: bool,

    /// Whether to enable mDNS peer discovery.
    pub mdns_enabled: bool,

    /// Whether to enable multicast.
    pub multicast_enabled: bool,
}

impl LanConfig {
    /// Create a new [`LanConfig`] with default settings.
    pub fn new() -> Self {
        Self {
            enabled: false,
            mdns_enabled: true,
            multicast_enabled: true,
        }
    }
}

/// Configuration for Apple Wireless Direct Link (AWDL) network transport.
/// Not supported on all platforms.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct AwdlConfig {
    /// Whether to enable the AWDL transport.
    pub enabled: bool,
}

impl AwdlConfig {
    /// Create a new [`AwdlConfig`] with default settings.
    pub fn new() -> Self {
        Self { enabled: false }
    }
}

/// Configuration for Wi-Fi Aware network transport.
/// Not supported on all platforms.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct WifiAwareConfig {
    /// Whether to enable the Wi-Fi Aware transport.
    pub enabled: bool,
}

impl WifiAwareConfig {
    /// Create a new [`WifiAwareConfig`] with default settings.
    pub fn new() -> Self {
        Self { enabled: false }
    }
}

/// Configuration for the multicast data-sync transport.
///
/// **Not the same as [`LanConfig::multicast_enabled`]**, which controls LAN's
/// mDNS-multicast peer discovery. This config enables a separate transport
/// that uses NORM (RFC 5740 NACK-Oriented Reliable Multicast) for data sync
/// over a configured multicast group — suitable for mesh / tactical-radio
/// scenarios where Bluetooth/AWDL/LAN aren't viable.
///
/// Behind the `__ditto_multicast_beta` Cargo feature in `ditto-peer`.
/// Setting `enabled = true` without that feature compiled into the FFI
/// is a no-op (logged but not fatal).
#[cfg(feature = "__ditto_multicast_beta")]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct MulticastConfig {
    /// Whether to enable the multicast transport.
    pub enabled: bool,
    /// IPv4 multicast group address (e.g. `224.1.2.3`). All peers in the
    /// mesh must use the same address.
    pub group_address: String,
    /// UDP port. All peers must use the same port.
    pub port: u16,
    /// Network interface name to bind to (e.g. `en0`, `wlan0`). When `None`,
    /// the OS picks one — typically the default route interface.
    pub interface: Option<String>,
}

#[cfg(feature = "__ditto_multicast_beta")]
impl MulticastConfig {
    /// Create a new [`MulticastConfig`] with default settings (disabled,
    /// historical multicast group `224.1.2.3:6003`, no interface override).
    pub fn new() -> Self {
        Self::default()
    }
}

#[cfg(feature = "__ditto_multicast_beta")]
impl Default for MulticastConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            group_address: "224.1.2.3".to_string(),
            port: 6003,
            interface: None,
        }
    }
}

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

    #[test]
    fn awdl_config_default_disabled() {
        assert!(!AwdlConfig::default().enabled);
        assert!(!AwdlConfig::new().enabled);
    }

    #[test]
    fn wifi_aware_config_default_disabled() {
        assert!(!WifiAwareConfig::default().enabled);
        assert!(!WifiAwareConfig::new().enabled);
    }

    #[test]
    fn enable_all_peer_to_peer_enables_all_transports() {
        let mut config = TransportConfig::new();
        config.enable_all_peer_to_peer();
        assert!(config.peer_to_peer.bluetooth_le.enabled);
        assert!(config.peer_to_peer.lan.enabled);
        assert!(config.peer_to_peer.awdl.enabled);
        assert!(config.peer_to_peer.wifi_aware.enabled);
    }

    #[test]
    fn awdl_config_serde_roundtrip() {
        let original = AwdlConfig { enabled: true };
        let json = serde_json::to_string(&original).unwrap();
        let decoded: AwdlConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(original, decoded);
    }

    #[test]
    fn wifi_aware_config_serde_roundtrip() {
        let original = WifiAwareConfig { enabled: true };
        let json = serde_json::to_string(&original).unwrap();
        let decoded: WifiAwareConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(original, decoded);
    }

    #[test]
    fn peer_to_peer_missing_fields_default_to_disabled() {
        let json = r#"{"bluetooth_le":{"enabled":true},"lan":{"enabled":true,"mdns_enabled":true,"multicast_enabled":true}}"#;
        let peer_to_peer: PeerToPeer = serde_json::from_str(json).unwrap();
        assert!(!peer_to_peer.awdl.enabled);
        assert!(!peer_to_peer.wifi_aware.enabled);
    }
}
