//! Config-driven storage backend factory
//!
//! Selects and constructs the appropriate [`Storage`] implementation based on
//! [`StorageConfig`](crate::config::schema::StorageConfig) at runtime —
//! no recompilation required to switch backends.
//!
//! ## Engine selection priority
//!
//! 1. `surrealdb-backend` feature ON **and** `secondary.enabled` **and**
//!    `secondary.type == surrealdb` → [`SurrealDbStorage`](super::surrealdb::SurrealDbStorage)
//!    using the dual hot/cold engine layout from `secondary.surrealdb`
//! 2. `primary == memory` → [`MemoryStorage`](super::memory::MemoryStorage)
//! 3. default → [`FilesystemStorage`](super::filesystem::FilesystemStorage)
//!    rooted at `base_path`
//!
//! ## NATS event wrapping
//!
//! When the `nats-events` feature is enabled, [`build_eventing`] wraps any
//! backend with [`EventingStorage`](crate::events::EventingStorage), publishing
//! a [`KogralEvent`](crate::events::KogralEvent) after each mutation.

use std::path::PathBuf;

use crate::config::schema::{StorageConfig, StorageType};
use crate::error::Result;
use crate::storage::{filesystem::FilesystemStorage, memory::MemoryStorage, Storage};

/// Build the storage backend driven by `config`.
///
/// Applies the engine selection priority described in the module docs.
/// For NATS event publishing after mutations, use [`build_eventing`] instead.
///
/// # Errors
///
/// Returns [`KbError`](crate::error::KbError) if the `SurrealDB` engine
/// connection fails.
// `unused_async`: this function has no await when surrealdb-backend is off,
// but callers treat it as async uniformly across all feature combinations.
#[allow(clippy::unused_async)]
pub async fn build(config: &StorageConfig, base_path: PathBuf) -> Result<Box<dyn Storage>> {
    #[cfg(feature = "surrealdb-backend")]
    if config.secondary.enabled
        && config.secondary.storage_type
            == crate::config::schema::SecondaryStorageType::Surrealdb
    {
        let db = crate::storage::surrealdb::SurrealDbStorage::from_config(
            &config.secondary.surrealdb,
        )
        .await?;
        return Ok(Box::new(db));
    }

    Ok(match config.primary {
        StorageType::Memory => Box::new(MemoryStorage::new()),
        StorageType::Filesystem => Box::new(FilesystemStorage::new(base_path)),
    })
}

/// Build storage wrapped with NATS `JetStream` event publishing.
///
/// Calls [`build`] to construct the base backend, then wraps it with
/// [`EventingStorage`](crate::events::EventingStorage) so that a
/// [`KogralEvent`](crate::events::KogralEvent) is published after every
/// mutation (`save_graph`, `save_node`, `delete_node`).
///
/// When `config.nats` is `None` the function returns the unwrapped base
/// backend — identical to calling [`build`] directly.
///
/// `default_graph` is used in `NodeSaved` events when `node.project` is
/// `None`.
///
/// # Errors
///
/// Returns [`KbError`](crate::error::KbError) if the base backend or the
/// NATS `JetStream` connection fails.
#[cfg(feature = "nats-events")]
pub async fn build_eventing(
    config: &StorageConfig,
    base_path: PathBuf,
    default_graph: impl Into<String>,
) -> Result<Box<dyn Storage>> {
    let base = build(config, base_path).await?;

    let Some(nats_cfg) = &config.nats else {
        return Ok(base);
    };

    let nats_config = platform_nats::NatsConfig::from(nats_cfg.clone());
    let stream = platform_nats::EventStream::connect(&nats_config)
        .await
        .map_err(|e| crate::error::KbError::Storage(format!("NATS connect: {e}")))?;

    let publisher = crate::events::EventPublisher::new(stream);
    Ok(Box::new(crate::events::EventingStorage::new(
        base,
        publisher,
        default_graph.into(),
    )))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::schema::{StorageConfig, StorageType};
    use tempfile::tempdir;

    #[tokio::test]
    async fn build_memory_backend() {
        let config = StorageConfig {
            primary: StorageType::Memory,
            ..StorageConfig::default()
        };
        let storage = build(&config, PathBuf::from("/tmp")).await;
        assert!(storage.is_ok(), "memory backend must construct without I/O");
    }

    #[tokio::test]
    async fn build_filesystem_backend() {
        let dir = tempdir().unwrap();
        let config = StorageConfig::default(); // primary defaults to Filesystem
        let storage = build(&config, dir.path().to_path_buf()).await;
        assert!(
            storage.is_ok(),
            "filesystem backend must construct for any valid path"
        );
    }

    #[tokio::test]
    async fn memory_backend_save_load_roundtrip() {
        use crate::models::Graph;

        let config = StorageConfig {
            primary: StorageType::Memory,
            ..StorageConfig::default()
        };
        let mut storage = build(&config, PathBuf::from("/tmp")).await.unwrap();

        let graph = Graph::new("test-graph".to_string());
        storage.save_graph(&graph).await.unwrap();

        let loaded = storage.load_graph("test-graph").await.unwrap();
        assert_eq!(loaded.name, "test-graph");
    }

    #[cfg(feature = "surrealdb-backend")]
    #[tokio::test]
    async fn build_surrealdb_mem_backend() {
        use crate::config::schema::{
            SecondaryStorageConfig, SecondaryStorageType, SurrealDbBackendConfig,
            SurrealEngineConfig,
        };

        let config = StorageConfig {
            secondary: SecondaryStorageConfig {
                enabled: true,
                storage_type: SecondaryStorageType::Surrealdb,
                surrealdb: SurrealDbBackendConfig {
                    graph: SurrealEngineConfig::Mem,
                    hot: SurrealEngineConfig::Mem,
                    namespace: "factory-test".to_string(),
                },
            },
            ..StorageConfig::default()
        };
        let storage = build(&config, PathBuf::from("/tmp")).await;
        assert!(storage.is_ok(), "surrealdb mem:// backend must connect");
    }
}
