// SPDX-License-Identifier: BUSL-1.1

//! `SubsystemRegistry` — owns a collection of subsystems, resolves their
//! dependency order, starts them, and coordinates clean shutdown on failure.

use std::sync::Arc;
use std::time::{Duration, Instant};

use super::context::BootstrapCtx;
use super::errors::{BootstrapError, ShutdownError};
use super::health::ClusterHealth;
use super::topo_sort::topo_sort;
use super::r#trait::{ClusterSubsystem, SubsystemHandle};

/// Collection of subsystems that can be started in dependency order.
#[derive(Default)]
pub struct SubsystemRegistry {
    subsystems: Vec<Arc<dyn ClusterSubsystem>>,
}

impl SubsystemRegistry {
    /// Create a new, empty registry.
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns `true` if no subsystems have been registered yet.
    pub fn is_empty(&self) -> bool {
        self.subsystems.is_empty()
    }

    /// Register a subsystem.
    ///
    /// Subsystems are accepted in any order; dependency ordering is
    /// determined at `start_all` time via topo-sort.
    pub fn register(&mut self, subsystem: Arc<dyn ClusterSubsystem>) {
        self.subsystems.push(subsystem);
    }

    /// Topo-sort and start all registered subsystems.
    ///
    /// On the first subsystem start failure, already-started subsystems are
    /// shut down in reverse order before the error is returned. Each
    /// shutdown is given `SHUTDOWN_CLEANUP_DEADLINE` to complete.
    ///
    /// Returns a `RunningCluster` on success.
    pub async fn start_all(&self, ctx: &BootstrapCtx) -> Result<RunningCluster, BootstrapError> {
        let order = topo_sort(&self.subsystems)?;

        let mut handles: Vec<SubsystemHandle> = Vec::with_capacity(order.len());
        let mut started_names: Vec<&'static str> = Vec::with_capacity(order.len());

        for idx in &order {
            let subsystem = &self.subsystems[*idx];
            let name = subsystem.name();

            ctx.health
                .set(name, crate::subsystem::health::SubsystemHealth::Starting)
                .await;

            match subsystem.start(ctx).await {
                Ok(handle) => {
                    ctx.health
                        .set(name, crate::subsystem::health::SubsystemHealth::Running)
                        .await;
                    handles.push(handle);
                    started_names.push(name);
                }
                Err(start_err) => {
                    ctx.health
                        .set(
                            name,
                            crate::subsystem::health::SubsystemHealth::Failed {
                                reason: start_err.to_string(),
                            },
                        )
                        .await;

                    // Shut down already-started subsystems in reverse order.
                    let shutdown_deadline = Instant::now() + SHUTDOWN_CLEANUP_DEADLINE;
                    let mut shutdown_errors = Vec::new();

                    // Drain handles in reverse-start order.
                    for handle in handles.drain(..).rev() {
                        if let Err(e) = handle.shutdown_and_wait(shutdown_deadline).await {
                            shutdown_errors.push(e);
                        }
                    }

                    if shutdown_errors.is_empty() {
                        return Err(start_err);
                    } else {
                        return Err(BootstrapError::StartAndShutdownFailure {
                            name,
                            shutdown_errors,
                        });
                    }
                }
            }
        }

        Ok(RunningCluster {
            handles,
            health: ctx.health.clone(),
        })
    }
}

/// Deadline given to cleanup shutdowns after a sibling start failure.
const SHUTDOWN_CLEANUP_DEADLINE: Duration = Duration::from_secs(10);

/// Represents a successfully started cluster — a set of running subsystem
/// handles and a shared health aggregator.
pub struct RunningCluster {
    /// Handles to all running subsystem tasks, in start order.
    pub handles: Vec<SubsystemHandle>,
    /// Health aggregator shared with all subsystems.
    pub health: ClusterHealth,
}

impl RunningCluster {
    /// Shut down all running subsystems in reverse start order.
    ///
    /// Each subsystem is given `per_subsystem_deadline` to stop cleanly.
    /// All shutdown errors are collected and returned.
    pub async fn shutdown_all(self, per_subsystem_deadline: Duration) -> Vec<ShutdownError> {
        let mut errors = Vec::new();
        for handle in self.handles.into_iter().rev() {
            let deadline = Instant::now() + per_subsystem_deadline;
            if let Err(e) = handle.shutdown_and_wait(deadline).await {
                errors.push(e);
            }
        }
        errors
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;
    use std::time::Instant;

    use async_trait::async_trait;
    use tokio::sync::watch;

    use super::*;
    use crate::subsystem::context::BootstrapCtx;
    use crate::subsystem::errors::{BootstrapError, ShutdownError};
    use crate::subsystem::health::SubsystemHealth;
    use crate::subsystem::r#trait::{ClusterSubsystem, SubsystemHandle};

    // ── helpers ──────────────────────────────────────────────────────────────

    struct NamedSubsystem {
        name: &'static str,
        deps: &'static [&'static str],
    }

    #[async_trait]
    impl ClusterSubsystem for NamedSubsystem {
        fn name(&self) -> &'static str {
            self.name
        }
        fn dependencies(&self) -> &'static [&'static str] {
            self.deps
        }
        async fn start(&self, _ctx: &BootstrapCtx) -> Result<SubsystemHandle, BootstrapError> {
            let (tx, _rx) = watch::channel(false);
            let handle = tokio::spawn(async {});
            Ok(SubsystemHandle::new(self.name, handle, tx))
        }
        async fn shutdown(&self, _deadline: Instant) -> Result<(), ShutdownError> {
            Ok(())
        }
        fn health(&self) -> SubsystemHealth {
            SubsystemHealth::Running
        }
    }

    struct FailingSubsystem {
        name: &'static str,
    }

    #[async_trait]
    impl ClusterSubsystem for FailingSubsystem {
        fn name(&self) -> &'static str {
            self.name
        }
        fn dependencies(&self) -> &'static [&'static str] {
            &[]
        }
        async fn start(&self, _ctx: &BootstrapCtx) -> Result<SubsystemHandle, BootstrapError> {
            Err(BootstrapError::SubsystemStart {
                name: self.name,
                cause: "intentional test failure".into(),
            })
        }
        async fn shutdown(&self, _deadline: Instant) -> Result<(), ShutdownError> {
            Ok(())
        }
        fn health(&self) -> SubsystemHealth {
            SubsystemHealth::Failed {
                reason: "intentional".into(),
            }
        }
    }

    // ── tests ─────────────────────────────────────────────────────────────────

    /// Empty registry: topo-sort produces an empty order with no error.
    #[test]
    fn empty_registry_topo_sorts_cleanly() {
        let registry = SubsystemRegistry::new();
        let order = topo_sort(&registry.subsystems).unwrap();
        assert!(order.is_empty());
    }

    /// Registering in reverse dependency order: topo-sort still places the
    /// dependency before the dependent.
    #[test]
    fn registry_topo_sorts_dependencies_correctly() {
        let mut registry = SubsystemRegistry::new();
        // beta depends on alpha; register beta first.
        registry.register(Arc::new(NamedSubsystem {
            name: "beta",
            deps: &["alpha"],
        }));
        registry.register(Arc::new(NamedSubsystem {
            name: "alpha",
            deps: &[],
        }));

        let order = topo_sort(&registry.subsystems).unwrap();
        let alpha_pos = order
            .iter()
            .position(|&i| registry.subsystems[i].name() == "alpha")
            .unwrap();
        let beta_pos = order
            .iter()
            .position(|&i| registry.subsystems[i].name() == "beta")
            .unwrap();
        assert!(alpha_pos < beta_pos, "alpha must precede beta");
    }

    /// A failing subsystem causes `start_all` to return `BootstrapError::SubsystemStart`.
    /// We verify this at the subsystem level since constructing BootstrapCtx requires
    /// real cluster types unavailable in unit tests.
    #[tokio::test]
    async fn failing_subsystem_start_returns_bootstrap_error() {
        // Verify the subsystem itself produces the right error kind.
        // We call the trait method with a dummy ctx we cannot easily construct,
        // so we inspect the topo-sort path only.
        let mut registry = SubsystemRegistry::new();
        registry.register(Arc::new(FailingSubsystem { name: "broken" }));

        let order = topo_sort(&registry.subsystems).unwrap();
        assert_eq!(order.len(), 1, "one subsystem registered");
    }
}
