use std::path::PathBuf;

use crate::replication::shard::ShardRouter;
use crate::replication::{RegionId, ShardId};

/// How tidalDB stores data.
///
/// `Ephemeral` keeps everything in memory -- ideal for tests and short-lived
/// processes. `Persistent` writes to an LSM-tree on disk (fjall).
///
/// # Examples
///
/// ```
/// use tidaldb::StorageMode;
///
/// let mode = StorageMode::Ephemeral;
/// assert_ne!(mode, StorageMode::Persistent);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StorageMode {
    /// In-memory only. No filesystem access. Data is lost on drop.
    Ephemeral,
    /// Durable storage backed by fjall (LSM-tree). Requires `data_dir`.
    Persistent,
}

/// Configuration for a tidalDB instance.
///
/// Constructed either directly or via [`super::TidalDbBuilder`].
///
/// # Defaults
///
/// The default configuration is ephemeral (in-memory) with no directory paths.
/// Persistent mode requires at least `data_dir` to be set.
///
/// # Examples
///
/// ```
/// use tidaldb::Config;
///
/// let cfg = Config::default();
/// // Default is ephemeral — no data directory required.
/// assert!(cfg.data_dir.is_none());
/// ```
#[derive(Debug, Clone)]
pub struct Config {
    /// Storage backend selection.
    pub mode: StorageMode,
    /// Root directory for persistent data. Required when `mode` is `Persistent`.
    pub data_dir: Option<PathBuf>,
    /// Override for the WAL directory. Defaults to `{data_dir}/wal`.
    pub wal_dir: Option<PathBuf>,
    /// Override for the cache directory. Defaults to `{data_dir}/cache`.
    pub cache_dir: Option<PathBuf>,
    /// Cluster configuration. Default: single-node.
    pub cluster: NodeConfig,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            mode: StorageMode::Ephemeral,
            data_dir: None,
            wal_dir: None,
            cache_dir: None,
            cluster: NodeConfig::default(),
        }
    }
}

/// Errors that arise during configuration validation.
///
/// These are always caller errors -- the configuration is invalid and must
/// be corrected before a tidalDB instance can be opened.
///
/// # Examples
///
/// ```
/// use tidaldb::ConfigError;
///
/// let err = ConfigError::MissingDataDir;
/// assert!(err.to_string().contains("data directory"));
/// ```
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    /// Persistent mode was selected but no data directory was provided.
    #[error("persistent mode requires a data directory")]
    MissingDataDir,
    /// A directory path was specified but does not exist on the filesystem.
    #[error("directory does not exist: {}", path.display())]
    DirectoryNotFound { path: PathBuf },
    /// A directory exists but the process does not have write permission.
    #[error("directory is not writable: {}", path.display())]
    NotWritable { path: PathBuf },
    /// The data directory is already locked by another process.
    #[error("data directory is already open by another process: {}", path.display())]
    DataDirLocked { path: PathBuf },
}

/// The role of this node in the cluster.
///
/// `Single` is the default -- a standalone node that acts as both leader
/// and follower. Used for embedded deployments.
///
/// `Leader` accepts writes and ships WAL segments to followers.
///
/// `Follower` only accepts replayed events; write calls return `ReadOnly`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum NodeRole {
    #[default]
    Single,
    Leader,
    Follower,
}

/// Cluster configuration for distributed tidalDB deployments.
///
/// Defaults produce a single-node configuration identical to M0-M7 behavior.
/// Embedded deployments that do not set any cluster fields get single-node.
#[derive(Debug, Clone)]
pub struct NodeConfig {
    /// The role of this node.
    pub role: NodeRole,
    /// This node's shard identity.
    pub shard_id: ShardId,
    /// This node's region identity.
    pub region_id: RegionId,
    /// Shards this node is aware of (including itself). Empty for single-node.
    pub peer_shards: Vec<ShardId>,
    /// Routing strategy for entity-to-shard assignment.
    /// Not serialized -- reconstructed from `peer_shards` on startup.
    pub router: ShardRouter,
}

impl Default for NodeConfig {
    fn default() -> Self {
        Self {
            role: NodeRole::Single,
            shard_id: ShardId::SINGLE,
            region_id: RegionId::SINGLE,
            peer_shards: vec![],
            router: ShardRouter::single(),
        }
    }
}

impl NodeConfig {
    /// Returns true if this is a standalone single-node deployment.
    #[must_use]
    pub fn is_single_node(&self) -> bool {
        self.role == NodeRole::Single
    }

    /// Returns true if this node accepts writes.
    #[must_use]
    pub const fn accepts_writes(&self) -> bool {
        matches!(self.role, NodeRole::Single | NodeRole::Leader)
    }
}

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

    #[test]
    fn default_config_is_ephemeral() {
        let cfg = Config::default();
        assert_eq!(cfg.mode, StorageMode::Ephemeral);
        assert!(cfg.data_dir.is_none());
        assert!(cfg.wal_dir.is_none());
        assert!(cfg.cache_dir.is_none());
    }

    #[test]
    fn config_error_display_missing_data_dir() {
        let e = ConfigError::MissingDataDir;
        assert_eq!(e.to_string(), "persistent mode requires a data directory");
    }

    #[test]
    fn config_error_display_directory_not_found() {
        let e = ConfigError::DirectoryNotFound {
            path: PathBuf::from("/nonexistent"),
        };
        assert!(e.to_string().contains("/nonexistent"));
        assert!(e.to_string().contains("does not exist"));
    }

    #[test]
    fn config_error_display_not_writable() {
        let e = ConfigError::NotWritable {
            path: PathBuf::from("/readonly"),
        };
        assert!(e.to_string().contains("/readonly"));
        assert!(e.to_string().contains("not writable"));
    }

    #[test]
    fn storage_mode_debug() {
        // Ensure Debug is derived and produces readable output.
        let s = format!("{:?}", StorageMode::Ephemeral);
        assert_eq!(s, "Ephemeral");
        let s = format!("{:?}", StorageMode::Persistent);
        assert_eq!(s, "Persistent");
    }

    #[test]
    fn config_debug() {
        let cfg = Config::default();
        let s = format!("{cfg:?}");
        assert!(s.contains("Ephemeral"));
    }

    #[test]
    fn config_error_display_data_dir_locked() {
        let e = ConfigError::DataDirLocked {
            path: PathBuf::from("/locked/dir"),
        };
        let s = e.to_string();
        assert!(s.contains("already open"), "got: {s}");
        assert!(s.contains("/locked/dir"), "got: {s}");
    }

    #[test]
    fn node_role_default_is_single() {
        assert_eq!(NodeRole::default(), NodeRole::Single);
    }

    #[test]
    fn node_role_debug() {
        assert_eq!(format!("{:?}", NodeRole::Single), "Single");
        assert_eq!(format!("{:?}", NodeRole::Leader), "Leader");
        assert_eq!(format!("{:?}", NodeRole::Follower), "Follower");
    }

    #[test]
    fn node_role_equality() {
        assert_eq!(NodeRole::Single, NodeRole::Single);
        assert_ne!(NodeRole::Leader, NodeRole::Follower);
        assert_ne!(NodeRole::Single, NodeRole::Leader);
    }

    #[test]
    fn node_role_clone() {
        let role = NodeRole::Leader;
        let cloned = role;
        assert_eq!(role, cloned);
    }

    #[test]
    fn node_role_serde_roundtrip() {
        for role in [NodeRole::Single, NodeRole::Leader, NodeRole::Follower] {
            let json = serde_json::to_string(&role).expect("serialize");
            let back: NodeRole = serde_json::from_str(&json).expect("deserialize");
            assert_eq!(role, back);
        }
    }

    // ── NodeConfig tests ──────────────────────────────────────────────────

    #[test]
    fn node_config_default() {
        let nc = NodeConfig::default();
        assert_eq!(nc.role, NodeRole::Single);
        assert_eq!(nc.shard_id, ShardId::SINGLE);
        assert_eq!(nc.region_id, RegionId::SINGLE);
        assert!(nc.peer_shards.is_empty());
        assert!(nc.is_single_node());
        assert!(nc.accepts_writes());
    }

    #[test]
    fn node_config_leader_accepts_writes() {
        let nc = NodeConfig {
            role: NodeRole::Leader,
            ..NodeConfig::default()
        };
        assert!(!nc.is_single_node());
        assert!(nc.accepts_writes());
    }

    #[test]
    fn node_config_follower_rejects_writes() {
        let nc = NodeConfig {
            role: NodeRole::Follower,
            ..NodeConfig::default()
        };
        assert!(!nc.is_single_node());
        assert!(!nc.accepts_writes());
    }

    #[test]
    fn config_has_cluster_field_defaulting_to_single_node() {
        let cfg = Config::default();
        assert!(cfg.cluster.is_single_node());
    }
}
