//! Session-TTL sweeper and internal accessor helpers for [`TidalDb`].

use std::sync::{Arc, atomic::Ordering};

use super::TidalDb;

impl TidalDb {
    // ── M7p2 Session Sweeper ──────────────────────────────────────────────────

    /// Scan all active sessions and close any that have exceeded their
    /// policy's `max_session_duration`.
    ///
    /// Called every 60s by the sweeper thread, and once during shutdown.
    pub(crate) fn sweep_expired_sessions(&self) {
        let now = std::time::Instant::now();
        let mut expired_ids = Vec::new();

        for entry in &self.sessions {
            let state = entry.value();
            let elapsed = now.duration_since(state.started_at);

            // Look up the policy's max_session_duration.
            let max_duration = self
                .schema_def
                .as_ref()
                .and_then(|s| s.session_policy(&state.policy_name))
                .map(|p| p.max_session_duration);

            if let Some(max) = max_duration
                && elapsed > max
            {
                expired_ids.push(state.id);
            }
        }

        if !expired_ids.is_empty() {
            tracing::info!(
                count = expired_ids.len(),
                "sweeper: closing expired sessions"
            );
        }

        for session_id in expired_ids {
            if let Err(e) = self.close_session_internal(session_id, true) {
                tracing::warn!(
                    error = %e,
                    session_id = %session_id,
                    "sweeper: failed to close expired session"
                );
            }
        }
    }

    /// Start the session TTL sweeper background thread.
    ///
    /// The sweeper wakes every 60 seconds (interruptible via 1s sleep
    /// intervals) and closes any sessions that have exceeded their policy's
    /// `max_session_duration`. It shuts down within ~1s when `db.close()` is
    /// called.
    ///
    /// The sweeper holds a `Weak<TidalDb>` reference so it does not prevent
    /// `Arc::try_unwrap` from succeeding. If the `Arc` is dropped before the
    /// sweeper next runs, the `Weak::upgrade()` fails and the thread exits
    /// silently.
    ///
    /// Store the db in an `Arc` first, then call this method. For
    /// ephemeral/test usage, use `force_sweep()` instead.
    ///
    /// # Panics
    ///
    /// Panics if the OS refuses to spawn a thread (extremely rare).
    pub fn start_sweeper(db: &Arc<Self>) {
        let db_weak = Arc::downgrade(db);
        let shutdown = Arc::clone(&db.shutdown_sweeper);
        let handle = std::thread::Builder::new()
            .name("tidaldb-session-sweeper".into())
            .spawn(move || {
                tracing::info!("session TTL sweeper started");
                loop {
                    // Break the 60s sleep into 1s intervals so shutdown is
                    // detected within ~1s.
                    for _ in 0u32..60 {
                        if shutdown.load(Ordering::Relaxed) {
                            tracing::info!("session TTL sweeper shutting down");
                            return;
                        }
                        std::thread::sleep(std::time::Duration::from_secs(1));
                    }

                    if shutdown.load(Ordering::Relaxed) {
                        return;
                    }

                    // Upgrade the Weak -- if the db was dropped, exit silently.
                    let Some(db) = db_weak.upgrade() else {
                        return;
                    };
                    db.sweep_expired_sessions();
                    drop(db); // release the temporary strong ref immediately
                }
            })
            .expect("failed to spawn session sweeper thread");

        if let Ok(mut guard) = db.sweeper_thread.lock() {
            *guard = Some(handle);
        }
    }

    /// Test-only: trigger a session sweep without waiting for the background thread.
    #[cfg(any(test, feature = "test-utils"))]
    pub fn force_sweep(&self) {
        self.sweep_expired_sessions();
    }

    /// Test-only: return the number of active rate-limiter buckets.
    #[cfg(any(test, feature = "test-utils"))]
    pub fn rate_limiter_bucket_count(&self) -> usize {
        self.rate_limiter.active_buckets()
    }

    /// Test-only: reference to the backup-in-progress flag.
    ///
    /// Used by integration tests to verify the backpressure mechanism
    /// without requiring a racy concurrent backup.
    #[cfg(any(test, feature = "test-utils"))]
    #[allow(clippy::missing_const_for_fn)] // Arc field prevents const in practice
    pub fn backup_in_progress_flag(&self) -> &Arc<std::sync::atomic::AtomicBool> {
        &self.backup_in_progress
    }

    /// Store the directory lock file so it is held for the database lifetime.
    ///
    /// The advisory flock is released automatically when the `File` is dropped.
    pub(crate) fn set_lock_file(&mut self, file: std::fs::File) {
        self.lock_file = Some(file);
    }

    /// Replace the rate limiter with one using the given config.
    ///
    /// Called by [`TidalDbBuilder::open`] before any sessions are opened, so
    /// there is no risk of orphaned bucket references.
    pub(crate) fn with_rate_limiter_config(
        mut self,
        config: crate::load::RateLimiterConfig,
    ) -> Self {
        self.rate_limiter = Arc::new(crate::load::RateLimiter::new(config));
        self
    }

    /// Reject write operations on follower nodes.
    ///
    /// Returns `TidalError::ReadOnly` when the node's role is `Follower`.
    /// Must be the **first** statement in every public write method so the
    /// guard fires before any validation or allocation.
    pub(crate) fn require_writeable(&self, operation: &str) -> crate::Result<()> {
        if self.config.cluster.role == crate::db::config::NodeRole::Follower {
            return Err(crate::schema::TidalError::read_only(operation));
        }
        Ok(())
    }

    // ── Internal helpers ─────────────────────────────────────────────────────

    /// Borrow the storage backend, or error if the database was opened without a schema.
    pub(crate) fn storage(&self) -> crate::Result<&super::storage_box::StorageBox> {
        self.storage.as_ref().ok_or_else(|| {
            crate::schema::TidalError::internal(
                "storage_access",
                "no storage: open with with_schema()",
            )
        })
    }

    /// Borrow the signal ledger, or error if the database was opened without a schema.
    #[allow(dead_code)] // Used by upcoming session signal write path.
    pub(crate) fn ledger(&self) -> crate::Result<&Arc<crate::signals::SignalLedger>> {
        self.ledger.as_ref().ok_or_else(|| {
            crate::schema::TidalError::internal(
                "ledger_access",
                "no ledger: open with with_schema()",
            )
        })
    }

    /// Reference to the co-engagement index (M6).
    #[must_use]
    pub fn co_engagement(&self) -> &crate::entities::CoEngagementIndex {
        &self.co_engagement
    }

    /// Reference to the cohort signal ledger (M6).
    #[must_use]
    pub fn cohort_ledger(&self) -> &crate::cohort::CohortSignalLedger {
        &self.cohort_ledger
    }
}
