use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use fs4::FileExt;

use super::TidalDb;
use super::config::{Config, ConfigError, StorageMode};
use super::metrics::MetricsState;
use super::paths::Paths;

use crate::load::RateLimiterConfig;
use crate::schema::Schema;

/// Fluent builder for constructing a [`TidalDb`] instance.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tidaldb::TidalDb;
///
/// // Ephemeral (in-memory) database — no filesystem access:
/// let db = TidalDb::builder().ephemeral().open()?;
/// db.health_check()?;
/// # Ok(())
/// # }
/// ```
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tidaldb::TidalDb;
///
/// // Persistent database with an explicit data directory:
/// let db = TidalDb::builder()
///     .with_data_dir("/var/lib/tidaldb")
///     .open()?;
/// # Ok(())
/// # }
/// ```
pub struct TidalDbBuilder {
    config: Config,
    /// Address for the optional metrics HTTP server (e.g. "127.0.0.1:9090").
    /// Only used when the `metrics` feature is enabled.
    #[allow(dead_code)]
    metrics_addr: Option<String>,
    /// Optional schema for signal types. When set, `open()` wires the storage
    /// engine, WAL, and signal ledger into the returned `TidalDb`.
    schema: Option<Schema>,
    /// Optional rate limiter configuration. When `None`, the default
    /// (unlimited) config is used.
    rate_limiter_config: Option<RateLimiterConfig>,
    /// Optional transport for replication.
    ///
    /// When set and `NodeRole::Follower`, the receiver is started automatically.
    /// When set and `NodeRole::Leader`, the WAL shipper is spawned automatically.
    transport: Option<std::sync::Arc<dyn crate::replication::Transport>>,
}

impl TidalDbBuilder {
    /// Create a new builder with default (ephemeral) configuration.
    #[must_use]
    pub fn new() -> Self {
        Self {
            config: Config::default(),
            metrics_addr: None,
            schema: None,
            rate_limiter_config: None,
            transport: None,
        }
    }

    /// Attach a validated [`Schema`] to this builder.
    ///
    /// When a schema is provided, [`open`](Self::open) wires in the storage
    /// engine, write-ahead log, and signal ledger, enabling the full M1 API
    /// (`write_item`, `signal`, `read_decay_score`, etc.).
    ///
    /// Without a schema the database still opens in M0 compatibility mode
    /// and only `health_check` and metrics are available.
    #[must_use]
    pub fn with_schema(mut self, schema: Schema) -> Self {
        self.schema = Some(schema);
        self
    }

    /// Configure per-agent session rate limiting.
    ///
    /// When set, each `(agent_id, session_id)` pair gets a token bucket with
    /// the specified burst capacity and sustained rate. Without this, rate
    /// limiting is disabled (unlimited by default).
    ///
    /// Use [`RateLimiterConfig::limited`] to create a limiting configuration.
    #[must_use]
    pub const fn with_rate_limiter_config(mut self, config: RateLimiterConfig) -> Self {
        self.rate_limiter_config = Some(config);
        self
    }

    /// Configure this instance for distributed deployment.
    ///
    /// Not required for single-node embedded use. The default `NodeConfig`
    /// produces a single-node configuration identical to M0-M7 behavior.
    #[must_use]
    pub fn with_cluster(mut self, config: super::config::NodeConfig) -> Self {
        self.config.cluster = config;
        self
    }

    /// Attach a replication transport.
    ///
    /// When the node role is `Follower`, `open()` automatically calls
    /// `start_replication(transport)` after the database is constructed.
    /// When the role is `Leader`, `open()` spawns the WAL shipper thread
    /// using this transport.
    ///
    /// Not required for single-node (`NodeRole::Single`) deployments.
    #[must_use]
    pub fn with_transport(
        mut self,
        transport: std::sync::Arc<dyn crate::replication::Transport>,
    ) -> Self {
        self.transport = Some(transport);
        self
    }

    /// Switch to ephemeral (in-memory) mode, clearing any directory paths.
    ///
    /// This is the default mode. Calling this is only necessary to reset
    /// a builder that was previously configured for persistent mode.
    #[must_use]
    pub fn ephemeral(mut self) -> Self {
        self.config.mode = StorageMode::Ephemeral;
        self.config.data_dir = None;
        self.config.wal_dir = None;
        self.config.cache_dir = None;
        self
    }

    /// Switch to persistent mode with the given data directory.
    ///
    /// The directory must exist and be writable at the time [`open`](Self::open)
    /// is called. WAL and cache directories default to subdirectories of
    /// `data_dir` unless explicitly overridden.
    #[must_use]
    pub fn with_data_dir(mut self, dir: impl AsRef<Path>) -> Self {
        self.config.mode = StorageMode::Persistent;
        self.config.data_dir = Some(dir.as_ref().to_path_buf());
        self
    }

    /// Override the WAL directory (defaults to `{data_dir}/wal`).
    #[must_use]
    pub fn wal_dir(mut self, dir: impl AsRef<Path>) -> Self {
        self.config.wal_dir = Some(dir.as_ref().to_path_buf());
        self
    }

    /// Override the cache directory (defaults to `{data_dir}/cache`).
    #[must_use]
    pub fn cache_dir(mut self, dir: impl AsRef<Path>) -> Self {
        self.config.cache_dir = Some(dir.as_ref().to_path_buf());
        self
    }

    /// Eagerly validate the configuration.
    ///
    /// Checks:
    /// - Persistent mode requires `data_dir`
    /// - All specified directories must exist and be writable
    ///
    /// # Errors
    ///
    /// Returns [`ConfigError`] describing the first validation failure.
    pub fn validate(&self) -> Result<(), ConfigError> {
        if self.config.mode == StorageMode::Persistent && self.config.data_dir.is_none() {
            return Err(ConfigError::MissingDataDir);
        }

        // Validate each specified directory exists and is writable.
        let dirs_to_check: Vec<&PathBuf> = [
            self.config.data_dir.as_ref(),
            self.config.wal_dir.as_ref(),
            self.config.cache_dir.as_ref(),
        ]
        .into_iter()
        .flatten()
        .collect();

        for dir in dirs_to_check {
            if !dir.exists() {
                return Err(ConfigError::DirectoryNotFound { path: dir.clone() });
            }
            // Check writability by querying metadata permissions.
            // On Unix, we check the readonly flag. A more robust check
            // would attempt to create a temp file, but metadata is
            // sufficient for configuration validation.
            if dir
                .metadata()
                .map(|m| m.permissions().readonly())
                .unwrap_or(true)
            {
                return Err(ConfigError::NotWritable { path: dir.clone() });
            }
        }

        Ok(())
    }

    /// Enable the metrics HTTP server on the given address.
    ///
    /// Only available when the `metrics` feature is enabled. When the
    /// feature is disabled, this method does not exist and the builder
    /// compiles without it.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # #[cfg(feature = "metrics")]
    /// # {
    /// let db = tidaldb::TidalDb::builder()
    ///     .ephemeral()
    ///     .enable_metrics("127.0.0.1:9090")
    ///     .open()?;
    /// if let Some(addr) = db.metrics_addr() {
    ///     println!("metrics at http://{addr}/metrics");
    /// }
    /// # }
    /// # Ok(())
    /// # }
    /// ```
    #[cfg(feature = "metrics")]
    #[must_use]
    pub fn enable_metrics(mut self, addr: impl Into<String>) -> Self {
        self.metrics_addr = Some(addr.into());
        self
    }

    /// Resolve default directory paths using [`Paths`] for persistent mode.
    ///
    /// When a `data_dir` is set and `wal_dir` or `cache_dir` are not
    /// explicitly overridden, this fills them in from [`Paths`]. This
    /// makes `Paths` the single source of truth for directory layout --
    /// the builder and the CLI both derive defaults the same way.
    fn resolve_defaults(&mut self) {
        if let Some(ref data_dir) = self.config.data_dir {
            let paths = Paths::new(data_dir);
            if self.config.wal_dir.is_none() {
                self.config.wal_dir = Some(paths.wal_dir());
            }
            if self.config.cache_dir.is_none() {
                self.config.cache_dir = Some(paths.cache_dir());
            }
        }
    }

    /// Validate and open a [`TidalDb`] instance.
    ///
    /// Calls [`validate`](Self::validate), then resolves default directory
    /// paths via [`Paths`], and constructs the database handle.
    ///
    /// Validation checks only user-specified directories. Resolved defaults
    /// (e.g., `{data_dir}/wal`) are populated after validation -- the storage
    /// engine will create them via [`Paths::ensure_all`] during initialization.
    ///
    /// # Panics
    ///
    /// Panics if the OS refuses to spawn the session TTL sweeper thread
    /// (extremely rare), or if `Arc::try_unwrap` fails after calling
    /// `start_sweeper` (impossible in practice: the sweeper holds only `Weak`).
    ///
    /// # Errors
    ///
    /// Returns [`crate::TidalError`] if validation fails or initialization
    /// encounters an error.
    #[tracing::instrument(skip(self), fields(mode = ?self.config.mode))]
    #[allow(clippy::too_many_lines)]
    pub fn open(mut self) -> crate::Result<TidalDb> {
        self.validate()?;
        self.resolve_defaults();

        // ── Fix A: Acquire exclusive advisory lock on the data directory ────
        // Persistent mode only. Prevents two processes from silently
        // corrupting state by opening the same directory simultaneously.
        // The lock is held for the lifetime of the TidalDb instance and
        // released automatically when the File handle is dropped.
        let lock_file = if let Some(ref data_dir) = self.config.data_dir {
            let lock_path = data_dir.join("tidaldb.lock");
            let file = OpenOptions::new()
                .write(true)
                .create(true)
                .truncate(false)
                .open(&lock_path)
                .map_err(|e| {
                    crate::TidalError::internal(
                        "open",
                        format!("failed to open lock file {}: {e}", lock_path.display()),
                    )
                })?;

            file.try_lock_exclusive().map_err(|_| {
                crate::TidalError::Config(ConfigError::DataDirLocked {
                    path: data_dir.clone(),
                })
            })?;

            Some(file)
        } else {
            None
        };

        let metrics = Arc::new(MetricsState::new());

        #[cfg(feature = "metrics")]
        let metrics_handle = if let Some(ref addr) = self.metrics_addr {
            let handle =
                super::http::MetricsHandle::start(addr, Arc::clone(&metrics)).map_err(|e| {
                    crate::TidalError::internal(
                        "open",
                        format!("metrics server failed to start: {e}"),
                    )
                })?;
            Some(handle)
        } else {
            None
        };

        if let Some(schema) = self.schema {
            // Wire in storage, WAL, signal ledger, and M2 indexes.
            let result = TidalDb::open_with_schema(&self.config, schema)?;

            // ── Fix B: Schema fingerprint persistence ───────────────────
            // Persistent mode only. Compute a BLAKE3 hash of the schema's
            // signal definitions (sorted by name, hashing half_life as
            // u128 nanos to avoid float instability). On first open the
            // fingerprint is written; on subsequent opens it is compared
            // against the stored value.
            if self.config.mode == StorageMode::Persistent {
                super::schema_fingerprint::verify_or_store_schema_fingerprint(
                    &result.schema_def,
                    result.storage.items_engine(),
                )?;
            }

            let mut db = TidalDb::from_parts(
                self.config,
                metrics,
                #[cfg(feature = "metrics")]
                metrics_handle,
                result,
            );

            // Transfer the lock file into the TidalDb so it lives for
            // the duration of the database instance.
            if let Some(lf) = lock_file {
                db.set_lock_file(lf);
            }

            if let Some(rl_config) = self.rate_limiter_config {
                db = db.with_rate_limiter_config(rl_config);
            }

            // Restore previously archived session snapshots from storage.
            db.restore_sessions();

            // Wire replication transport based on node role.
            if let Some(transport) = self.transport {
                use super::config::NodeRole;
                match db.config.cluster.role {
                    NodeRole::Follower => {
                        // Start the segment receiver thread.
                        db.start_replication(transport).map_err(|e| {
                            crate::TidalError::internal(
                                "open",
                                format!("failed to start replication receiver: {e}"),
                            )
                        })?;
                        tracing::info!(
                            shard = %db.config.cluster.shard_id,
                            "follower: replication receiver started"
                        );
                    }
                    NodeRole::Leader => {
                        // Spawn the WAL shipper thread (persistent mode only: needs WAL dir).
                        if let Some(ref wal_dir) = db.config.wal_dir {
                            let shipper_config = crate::replication::ShipperConfig {
                                wal_dir: wal_dir.clone(),
                                shard_id: db.config.cluster.shard_id,
                                peer_shards: db.config.cluster.peer_shards.clone(),
                                poll_interval: std::time::Duration::from_secs(2),
                            };
                            let handle =
                                crate::replication::spawn_shipper(shipper_config, transport);
                            db.shipper_handle = Some(handle);
                            tracing::info!(
                                shard = %db.config.cluster.shard_id,
                                peers = ?db.config.cluster.peer_shards,
                                "leader: WAL shipper started"
                            );
                        } else {
                            tracing::warn!(
                                "leader role configured but no WAL dir available; \
                                 shipper not started (ephemeral mode)"
                            );
                        }
                    }
                    NodeRole::Single => {
                        // Single-node: no replication threads needed.
                        tracing::debug!("transport provided but NodeRole::Single; ignored");
                    }
                }
            }

            // Start the session TTL sweeper for persistent-mode databases.
            // Ephemeral/test databases use force_sweep() for explicit control.
            if db.config.mode == StorageMode::Persistent {
                // Temporarily wrap in Arc so start_sweeper can downgrade to Weak.
                // The spawned thread holds only Weak<TidalDb>, so try_unwrap succeeds
                // immediately -- the thread sleeps 60s before its first Weak::upgrade().
                let db_arc = Arc::new(db);
                TidalDb::start_sweeper(&db_arc);
                db = Arc::try_unwrap(db_arc)
                    .expect("sweeper holds only Weak: no strong Arc can exist at open time");
            }

            tracing::info!(
                mode = ?db.config.mode,
                data_dir = ?db.config.data_dir,
                items = db.item_count(),
                "tidaldb opened with schema"
            );

            Ok(db)
        } else {
            // M0 compatibility mode: no storage, no ledger, no WAL.
            let mut db = TidalDb::from_config(
                self.config,
                metrics,
                #[cfg(feature = "metrics")]
                metrics_handle,
            );

            if let Some(lf) = lock_file {
                db.set_lock_file(lf);
            }

            if let Some(rl_config) = self.rate_limiter_config {
                db = db.with_rate_limiter_config(rl_config);
            }

            tracing::info!(mode = ?db.config.mode, "tidaldb opened (no schema)");

            Ok(db)
        }
    }
}

impl Default for TidalDbBuilder {
    fn default() -> Self {
        Self::new()
    }
}

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