//! Runtime-neutral singleton registration for native `GridClient` composition.

use std::sync::{Arc, Mutex};

use crate::{Error, GridClient, Settings};

/// Factory stored by service collections for lazy `GridClient` construction.
pub type GridClientFactory = Arc<dyn Fn() -> Result<GridClient, Error> + Send + Sync>;

/// Object-safe service collection contract used by the mapped DI extension.
pub trait IServiceCollection: Send {
    /// Registers the concrete `GridClient` singleton factory.
    fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error>;

    /// Registers `IGridClient` as an alias of the concrete singleton.
    fn add_i_grid_client_alias(&mut self) -> Result<(), Error>;
}

/// Small native service collection suitable for applications without a DI crate.
#[derive(Default)]
pub struct ServiceCollection {
    client_factory: Option<GridClientFactory>,
    client: Mutex<Option<GridClient>>,
    interface_alias: bool,
}

impl ServiceCollection {
    /// Resolves the lazily-created concrete client singleton.
    pub fn resolve_grid_client(&self) -> Result<GridClient, Error> {
        let mut singleton = self
            .client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(client) = singleton.as_ref() {
            return Ok(client.clone());
        }
        let factory = self
            .client_factory
            .as_ref()
            .ok_or(Error::InvalidOperation)?;
        let client = factory()?;
        *singleton = Some(client.clone());
        Ok(client)
    }

    /// Resolves the interface registration to the same native singleton.
    pub fn resolve_i_grid_client(&self) -> Result<GridClient, Error> {
        if !self.interface_alias {
            return Err(Error::InvalidOperation);
        }
        self.resolve_grid_client()
    }
}

impl IServiceCollection for ServiceCollection {
    fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> {
        if self.client_factory.is_some() {
            return Err(Error::InvalidOperation);
        }
        self.client_factory = Some(factory);
        Ok(())
    }

    fn add_i_grid_client_alias(&mut self) -> Result<(), Error> {
        if self.client_factory.is_none() || self.interface_alias {
            return Err(Error::InvalidOperation);
        }
        self.interface_alias = true;
        Ok(())
    }
}

pub(crate) fn add_grid_client(
    mut services: Box<dyn IServiceCollection>,
    configure: Option<Box<dyn Fn(&mut Settings) + Send + Sync>>,
) -> Result<Box<dyn IServiceCollection>, Error> {
    let configure: Option<Arc<dyn Fn(&mut Settings) + Send + Sync>> = configure.map(Arc::from);
    let factory: GridClientFactory = Arc::new(move || {
        let mut settings = Settings::default();
        if let Some(configure) = &configure {
            configure(&mut settings);
        }
        GridClient::builder()
            .with_settings(settings)
            .build()
            .map_err(Into::into)
    });
    services.add_grid_client_singleton(factory)?;
    services.add_i_grid_client_alias()?;
    Ok(services)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};

    #[derive(Default)]
    struct RecordingCollection {
        factory: Arc<Mutex<Option<GridClientFactory>>>,
        alias_registered: Arc<AtomicBool>,
    }

    impl IServiceCollection for RecordingCollection {
        fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> {
            *self
                .factory
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(factory);
            Ok(())
        }

        fn add_i_grid_client_alias(&mut self) -> Result<(), Error> {
            self.alias_registered.store(true, Ordering::Relaxed);
            Ok(())
        }
    }

    #[test]
    fn registers_one_lazy_client_and_interface_alias() {
        let calls = Arc::new(AtomicUsize::new(0));
        let observed = Arc::clone(&calls);
        let collection = RecordingCollection::default();
        let recorded_factory = Arc::clone(&collection.factory);
        let alias_registered = Arc::clone(&collection.alias_registered);
        let _services = add_grid_client(
            Box::new(collection),
            Some(Box::new(move |settings| {
                observed.fetch_add(1, Ordering::Relaxed);
                settings.default_effect_color.r = 0.25;
            })),
        )
        .unwrap();
        assert!(alias_registered.load(Ordering::Relaxed));
        assert_eq!(calls.load(Ordering::Relaxed), 0);

        let factory = recorded_factory
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
            .expect("GridClient factory must be registered");
        let client = factory().unwrap();
        assert_eq!(calls.load(Ordering::Relaxed), 1);
        assert_eq!(client.settings_ref().default_effect_color.r, 0.25);
    }

    #[test]
    fn concrete_collection_resolves_same_runtime() {
        let calls = Arc::new(AtomicUsize::new(0));
        let observed = Arc::clone(&calls);
        let mut services = ServiceCollection::default();
        services
            .add_grid_client_singleton(Arc::new(move || {
                observed.fetch_add(1, Ordering::Relaxed);
                GridClient::builder().build().map_err(Into::into)
            }))
            .unwrap();
        services.add_i_grid_client_alias().unwrap();
        let concrete = services.resolve_grid_client().unwrap();
        let interface = services.resolve_i_grid_client().unwrap();
        assert_eq!(concrete.settings_ref(), interface.settings_ref());
        assert_eq!(calls.load(Ordering::Relaxed), 1);
    }
}
