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)]
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(),
            },
            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;
    }
}

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

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

/// Configure outbound transports such as TCP and WebSocket dialing.
#[serde_as]
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
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)]
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)]
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 chose 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)]
pub struct HttpListenConfig {
    /// Whether inbound HTTP is enabled.
    pub enabled: bool,

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

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

    /// An optional path to static files to make available to clients.
    pub static_content_path: Option<PathBuf>,

    /// 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,
            static_content_path: None,
            websocket_sync: true,
            tls_key_path: None,
            tls_certificate_path: None,
        }
    }
}

/// Configure inbound TCP transports.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
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)]
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)]
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,
        }
    }
}
