//! Cluster configuration types.
//!
//! This module provides configuration for all aspects of cluster operation:
//!
//! - [`SwimConfig`]: SWIM protocol parameters (timeouts, intervals)
//! - [`ShardingConfig`]: Data sharding parameters (shard count, replication)
//! - [`ClusterConfig`]: Top-level configuration combining all settings

use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::time::Duration;

use crate::membership::NodeId;

/// Configuration for the SWIM membership protocol.
///
/// These parameters control the gossip protocol behavior including
/// how quickly failures are detected and how aggressively probing occurs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwimConfig {
    /// Interval between gossip rounds (piggybacked membership updates).
    ///
    /// Lower values mean faster convergence but more network traffic.
    /// Default: 200ms
    pub gossip_interval: Duration,

    /// Interval between ping probes to random members.
    ///
    /// Each round, the node pings one random peer to check liveness.
    /// Default: 1s
    pub probe_interval: Duration,

    /// How long to wait for a probe response before declaring failure.
    ///
    /// After this timeout, indirect probing begins.
    /// Default: 500ms
    pub probe_timeout: Duration,

    /// How long a node stays in Suspect state before being declared Dead.
    ///
    /// Longer values reduce false positives but delay failure detection.
    /// Default: 5s
    pub suspicion_timeout: Duration,

    /// Number of random members to ask for indirect probes.
    ///
    /// When direct probe fails, we ask K peers to probe the target.
    /// Higher values increase reliability but use more bandwidth.
    /// Default: 3
    pub indirect_probe_count: usize,

    /// Maximum size of the gossip message queue.
    ///
    /// Limits memory usage for pending gossip messages.
    /// Default: 1000
    pub gossip_queue_size: usize,

    /// Number of times to retransmit a membership update.
    ///
    /// Higher values ensure updates reach all nodes but increase traffic.
    /// Default: 3
    pub retransmit_multiplier: usize,

    /// Port for SWIM protocol UDP messages.
    ///
    /// Default: 18183 (part of StemeDB 181XX port scheme)
    pub swim_port: u16,
}

impl Default for SwimConfig {
    fn default() -> Self {
        Self {
            gossip_interval: Duration::from_millis(200),
            probe_interval: Duration::from_secs(1),
            probe_timeout: Duration::from_millis(500),
            suspicion_timeout: Duration::from_secs(5),
            indirect_probe_count: 3,
            gossip_queue_size: 1000,
            retransmit_multiplier: 3,
            swim_port: 18183,
        }
    }
}

impl SwimConfig {
    /// Creates a new SwimConfig with default values.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the gossip interval.
    #[must_use]
    pub fn with_gossip_interval(mut self, interval: Duration) -> Self {
        self.gossip_interval = interval;
        self
    }

    /// Sets the probe interval.
    #[must_use]
    pub fn with_probe_interval(mut self, interval: Duration) -> Self {
        self.probe_interval = interval;
        self
    }

    /// Sets the probe timeout.
    #[must_use]
    pub fn with_probe_timeout(mut self, timeout: Duration) -> Self {
        self.probe_timeout = timeout;
        self
    }

    /// Sets the suspicion timeout.
    #[must_use]
    pub fn with_suspicion_timeout(mut self, timeout: Duration) -> Self {
        self.suspicion_timeout = timeout;
        self
    }

    /// Sets the indirect probe count.
    #[must_use]
    pub fn with_indirect_probe_count(mut self, count: usize) -> Self {
        self.indirect_probe_count = count;
        self
    }

    /// Sets the SWIM port.
    #[must_use]
    pub fn with_swim_port(mut self, port: u16) -> Self {
        self.swim_port = port;
        self
    }

    /// Returns a "fast" configuration for testing.
    ///
    /// Uses shorter timeouts for quicker failure detection.
    #[must_use]
    pub fn fast() -> Self {
        Self {
            gossip_interval: Duration::from_millis(50),
            probe_interval: Duration::from_millis(200),
            probe_timeout: Duration::from_millis(100),
            suspicion_timeout: Duration::from_secs(1),
            indirect_probe_count: 2,
            gossip_queue_size: 100,
            retransmit_multiplier: 2,
            swim_port: 18183,
        }
    }
}

/// Configuration for data sharding across the cluster.
///
/// Controls how data is distributed and replicated across nodes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShardingConfig {
    /// Initial number of shards.
    ///
    /// Should be a power of 2 for efficient jump hash distribution.
    /// More shards allow finer-grained load balancing but increase overhead.
    /// Default: 16
    pub num_shards: u32,

    /// Number of replicas for each shard.
    ///
    /// Higher values increase fault tolerance but require more storage.
    /// Must be <= number of nodes in the cluster.
    /// Default: 3
    pub replication_factor: u32,

    /// Size threshold (bytes) at which a shard should split.
    ///
    /// When a shard exceeds this size, it's split into two smaller shards.
    /// Default: 64MB
    pub split_threshold_bytes: u64,

    /// Size threshold (bytes) below which adjacent shards should merge.
    ///
    /// When two adjacent shards are both below this threshold combined,
    /// they may be merged into one.
    /// Default: 20MB
    pub merge_threshold_bytes: u64,

    /// Minimum number of healthy replicas before write is accepted.
    ///
    /// Lower values allow more write availability during failures.
    /// Default: 1 (eventual consistency)
    pub min_write_replicas: u32,

    /// Number of replicas to read from for quorum reads.
    ///
    /// Set to replication_factor/2 + 1 for strong consistency.
    /// Default: 1 (eventual consistency)
    pub read_quorum: u32,
}

impl Default for ShardingConfig {
    fn default() -> Self {
        Self {
            num_shards: 16,
            replication_factor: 3,
            split_threshold_bytes: 64 * 1024 * 1024, // 64MB
            merge_threshold_bytes: 20 * 1024 * 1024, // 20MB
            min_write_replicas: 1,
            read_quorum: 1,
        }
    }
}

impl ShardingConfig {
    /// Creates a new ShardingConfig with default values.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the number of shards.
    #[must_use]
    pub fn with_num_shards(mut self, num: u32) -> Self {
        self.num_shards = num;
        self
    }

    /// Sets the replication factor.
    #[must_use]
    pub fn with_replication_factor(mut self, factor: u32) -> Self {
        self.replication_factor = factor;
        self
    }

    /// Sets the split threshold.
    #[must_use]
    pub fn with_split_threshold(mut self, bytes: u64) -> Self {
        self.split_threshold_bytes = bytes;
        self
    }

    /// Sets the merge threshold.
    #[must_use]
    pub fn with_merge_threshold(mut self, bytes: u64) -> Self {
        self.merge_threshold_bytes = bytes;
        self
    }

    /// Returns a configuration optimized for small clusters (1-3 nodes).
    #[must_use]
    pub fn small_cluster() -> Self {
        Self {
            num_shards: 4,
            replication_factor: 2,
            split_threshold_bytes: 32 * 1024 * 1024,
            merge_threshold_bytes: 10 * 1024 * 1024,
            min_write_replicas: 1,
            read_quorum: 1,
        }
    }

    /// Returns a configuration optimized for testing.
    #[must_use]
    pub fn testing() -> Self {
        Self {
            num_shards: 4,
            replication_factor: 2,
            split_threshold_bytes: 1024 * 1024, // 1MB
            merge_threshold_bytes: 256 * 1024,  // 256KB
            min_write_replicas: 1,
            read_quorum: 1,
        }
    }
}

/// Top-level cluster configuration.
///
/// Combines node identity, network addresses, and all protocol configurations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterConfig {
    /// Unique identifier for this node.
    pub node_id: NodeId,

    /// Address for RPC communication (gRPC sync protocol).
    pub rpc_addr: SocketAddr,

    /// Address for HTTP API (client-facing).
    pub api_addr: SocketAddr,

    /// List of seed node addresses for initial cluster discovery.
    ///
    /// At least one seed node must be reachable to join an existing cluster.
    /// For a new cluster, this can be empty (this node is the seed).
    pub seed_nodes: Vec<SocketAddr>,

    /// SWIM membership protocol configuration.
    pub swim: SwimConfig,

    /// Data sharding configuration.
    pub sharding: ShardingConfig,

    /// Whether this node should act as a gateway.
    ///
    /// Gateway nodes route client requests but don't store data.
    pub is_gateway: bool,

    /// Datacenter or region identifier.
    ///
    /// Used for rack-aware replica placement.
    pub datacenter: Option<String>,

    /// Rack or availability zone identifier.
    pub rack: Option<String>,
}

impl ClusterConfig {
    /// Creates a new ClusterConfig builder.
    #[must_use]
    pub fn builder() -> ClusterConfigBuilder {
        ClusterConfigBuilder::default()
    }

    /// Returns the swim port for this node based on config.
    #[must_use]
    pub fn swim_addr(&self) -> SocketAddr {
        let mut addr = self.rpc_addr;
        addr.set_port(self.swim.swim_port);
        addr
    }
}

/// Builder for ClusterConfig.
#[derive(Debug, Default)]
pub struct ClusterConfigBuilder {
    node_id: Option<NodeId>,
    rpc_addr: Option<SocketAddr>,
    api_addr: Option<SocketAddr>,
    seed_nodes: Vec<SocketAddr>,
    swim: SwimConfig,
    sharding: ShardingConfig,
    is_gateway: bool,
    datacenter: Option<String>,
    rack: Option<String>,
}

impl ClusterConfigBuilder {
    /// Sets the node ID.
    #[must_use]
    pub fn with_node_id(mut self, id: NodeId) -> Self {
        self.node_id = Some(id);
        self
    }

    /// Sets the RPC address.
    #[must_use]
    pub fn with_rpc_addr(mut self, addr: SocketAddr) -> Self {
        self.rpc_addr = Some(addr);
        self
    }

    /// Sets the API address.
    #[must_use]
    pub fn with_api_addr(mut self, addr: SocketAddr) -> Self {
        self.api_addr = Some(addr);
        self
    }

    /// Adds a seed node address.
    #[must_use]
    pub fn with_seed_node(mut self, addr: SocketAddr) -> Self {
        self.seed_nodes.push(addr);
        self
    }

    /// Sets the seed nodes.
    #[must_use]
    pub fn with_seed_nodes(mut self, addrs: Vec<SocketAddr>) -> Self {
        self.seed_nodes = addrs;
        self
    }

    /// Sets the SWIM configuration.
    #[must_use]
    pub fn with_swim_config(mut self, config: SwimConfig) -> Self {
        self.swim = config;
        self
    }

    /// Sets the sharding configuration.
    #[must_use]
    pub fn with_sharding_config(mut self, config: ShardingConfig) -> Self {
        self.sharding = config;
        self
    }

    /// Sets whether this is a gateway node.
    #[must_use]
    pub fn as_gateway(mut self, is_gateway: bool) -> Self {
        self.is_gateway = is_gateway;
        self
    }

    /// Sets the datacenter.
    #[must_use]
    pub fn with_datacenter(mut self, dc: impl Into<String>) -> Self {
        self.datacenter = Some(dc.into());
        self
    }

    /// Sets the rack.
    #[must_use]
    pub fn with_rack(mut self, rack: impl Into<String>) -> Self {
        self.rack = Some(rack.into());
        self
    }

    /// Builds the ClusterConfig.
    ///
    /// # Errors
    ///
    /// Returns an error if required fields are missing.
    pub fn build(self) -> crate::Result<ClusterConfig> {
        let rpc_addr = self
            .rpc_addr
            .ok_or_else(|| crate::ClusterError::Config("rpc_addr is required".to_string()))?;

        let api_addr = self
            .api_addr
            .ok_or_else(|| crate::ClusterError::Config("api_addr is required".to_string()))?;

        Ok(ClusterConfig {
            node_id: self.node_id.unwrap_or_else(NodeId::random),
            rpc_addr,
            api_addr,
            seed_nodes: self.seed_nodes,
            swim: self.swim,
            sharding: self.sharding,
            is_gateway: self.is_gateway,
            datacenter: self.datacenter,
            rack: self.rack,
        })
    }
}

#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;
