//! Diagnostics and health check methods for [`TidalDb`].

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

use super::{MetricsState, TidalDb};

impl TidalDb {
    /// Returns a reference to the shared metrics state.
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // Arc field prevents const in practice
    pub fn metrics(&self) -> &Arc<MetricsState> {
        &self.metrics
    }

    /// Access the load detector for metrics and health check reporting.
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // Arc field prevents const in practice
    pub fn load_detector(&self) -> &crate::load::LoadDetector {
        &self.load_detector
    }

    /// Returns `Ok(())` if the database is initialized and operational.
    ///
    /// Derives degraded state from real conditions, not just the explicit
    /// `close()` flag (HEALTH-1):
    /// - **closed** — the database has been shut down,
    /// - **dead checkpoint thread** — the periodic checkpoint join handle has
    ///   finished while the database is still open (panic, or an unexpected
    ///   early exit). A dead checkpoint thread means signal durability has
    ///   silently stopped, so it is latched into `metrics.checkpoint_thread_died`
    ///   so `/healthz` and `/metrics` (served from `MetricsState` alone) also
    ///   report degraded,
    /// - **stale / failing checkpoints** — surfaced via
    ///   [`MetricsState::is_degraded`], which folds in checkpoint-age staleness
    ///   and the latched dead-thread flag,
    /// - **unhealthy text index** — either text syncer has reported unhealthy
    ///   (an unrecoverable commit failure or exhausted retry budget). This is
    ///   read from [`TextIndex::is_healthy`](crate::text::TextIndex::is_healthy)
    ///   directly (NOT `JoinHandle::is_finished`: the syncer now survives commit
    ///   failures, so a live handle no longer implies a healthy index) and from
    ///   the latch the periodic refresher sets.
    /// - **dead replication receiver** — on a follower, the segment-receiver
    ///   thread halted on an apply error (corrupt segment / follower-WAL IO
    ///   fault). Replication has silently stopped applying, so the follower is
    ///   frozen. This is read from
    ///   [`SegmentReceiverHandle::died`](crate::replication::receiver::SegmentReceiverHandle::died),
    ///   which is set ONLY on the error halt path — a clean shutdown does not
    ///   flag it (C11).
    ///
    /// # Errors
    ///
    /// Returns an error if the database has been closed or a real degraded
    /// condition is detected.
    #[tracing::instrument(skip(self))]
    pub fn health_check(&self) -> crate::Result<()> {
        if self.closed.load(Ordering::Acquire) {
            self.metrics.health_ok.store(false, Ordering::Release);
            return Err(crate::TidalError::internal(
                "health_check",
                "database is closed",
            ));
        }

        // Real liveness: if the checkpoint thread's join handle has finished
        // while we are still open, durability has silently stopped. Latch the
        // flag so the HTTP-served health surfaces also report degraded.
        let checkpoint_dead = self
            .checkpoint_thread
            .lock()
            .ok()
            .and_then(|guard| guard.as_ref().map(std::thread::JoinHandle::is_finished))
            .unwrap_or(false);
        if checkpoint_dead {
            self.metrics
                .checkpoint_thread_died
                .store(true, Ordering::Release);
        }

        if self.metrics.is_degraded() {
            return Err(crate::TidalError::internal(
                "health_check",
                "database is degraded (dead/stale checkpoint thread or failed flush)",
            ));
        }

        // Replication-receiver liveness (C11): on a follower, a receiver that
        // halted on an apply error means replication silently stopped — the node
        // applies nothing and lag freezes flat instead of growing. Surface it as
        // degraded. `died()` is set only on the error halt path (a clean shutdown
        // leaves it false via the recv_segment()==None branch), and is a cheap
        // lock-free atomic read, so polling it under the receiver-handle guard is
        // safe even while the receiver thread is still parked. A poisoned guard is
        // recovered (PoisonError::into_inner) rather than masking the check.
        let receiver_dead = self
            .receiver_handle
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .as_ref()
            .is_some_and(crate::replication::receiver::SegmentReceiverHandle::died);
        if receiver_dead {
            return Err(crate::TidalError::internal(
                "health_check",
                "database is degraded (replication segment receiver halted on an \
                 apply error; the follower has silently stopped applying)",
            ));
        }

        // Text-search liveness. A missing syncer (no text fields in the schema)
        // is healthy by definition; a present-but-unhealthy syncer means text
        // search returns stale/empty results until a rebuild succeeds. Mark the
        // DB degraded so an operator notices, and latch it for the HTTP surface.
        if self.text_index_unhealthy() {
            return Err(crate::TidalError::internal(
                "health_check",
                "database is degraded (text index syncer unhealthy)",
            ));
        }
        Ok(())
    }

    /// Whether either text syncer is currently unhealthy.
    ///
    /// Reads the live [`TextIndex::is_healthy`](crate::text::TextIndex::is_healthy)
    /// for both the item and creator syncers, OR-folded with the latch the
    /// periodic refresher sets. A `None` syncer (no text fields declared) is
    /// healthy by definition.
    #[must_use]
    pub(super) fn text_index_unhealthy(&self) -> bool {
        if self
            .text_index_unhealthy
            .load(std::sync::atomic::Ordering::Acquire)
        {
            return true;
        }
        let item_ok = self.text_index.as_ref().is_none_or(|idx| idx.is_healthy());
        let creator_ok = self
            .creator_text_index
            .as_ref()
            .is_none_or(|idx| idx.is_healthy());
        let unhealthy = !item_ok || !creator_ok;
        if unhealthy {
            // Latch so the HTTP-served surfaces and subsequent reads agree.
            self.text_index_unhealthy
                .store(true, std::sync::atomic::Ordering::Release);
        }
        unhealthy
    }
}
