//! Lifecycle methods for [`TidalDb`]: `close`, `shutdown`, `shutdown_inner`,
//! `Drop`, and `Debug` implementations.

use std::sync::atomic::Ordering;

use super::{TidalDb, text_syncer::shutdown_text_syncer};
use crate::schema::{DurabilityError, TidalError, Timestamp};

impl TidalDb {
    // ── Lifecycle ─────────────────────────────────────────────────────────────

    /// Cleanly shut down the database.
    ///
    /// 1. Checkpoints all in-memory signal state to durable storage.
    /// 2. Flushes the storage engine.
    /// 3. Writes a WAL checkpoint marker and truncates old segments.
    /// 4. Shuts down the WAL writer thread.
    ///
    /// # Errors
    ///
    /// Returns an error if WAL shutdown fails, or if any final durable flush
    /// fails — the signal-ledger checkpoint, cohort/co-engagement checkpoints,
    /// the storage flush, or the WAL checkpoint marker (SHUTDOWN-2). All steps
    /// are attempted regardless; the first failure is returned so a caller that
    /// `?`-propagates `close()` does not exit 0 on lost durable state.
    #[tracing::instrument(skip(self))]
    pub fn close(self) -> crate::Result<()> {
        self.shutdown_inner()
    }

    /// Alias for [`close`](Self::close). Cleanly shuts down the database.
    ///
    /// # Errors
    ///
    /// Returns an error if WAL shutdown or any final durable flush fails — see
    /// [`close`](Self::close).
    pub fn shutdown(self) -> crate::Result<()> {
        self.close()
    }

    /// Internal shutdown logic shared by `close()` and `Drop`.
    #[allow(clippy::too_many_lines)]
    pub(crate) fn shutdown_inner(&self) -> crate::Result<()> {
        // CAS: first caller to flip false -> true executes the shutdown body.
        if self
            .closed
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_err()
        {
            // Already closed -- idempotent.
            return Ok(());
        }

        tracing::debug!(mode = ?self.config.mode, "tidaldb shutting down");

        // Mark health as degraded so the metrics endpoint reflects shutdown.
        self.metrics.health_ok.store(false, Ordering::Release);

        // M8p2: Join the segment receiver thread before WAL shuts down.
        // The receiver may hold Arc<SignalLedger> and the transport may block
        // in recv_segment until the sender side is dropped.
        if let Ok(mut guard) = self.receiver_handle.lock()
            && let Some(handle) = guard.take()
            && let Err(e) = handle.join()
        {
            tracing::warn!(error = %e, "segment receiver exited with corruption error");
        }

        // M7p2: Signal sweeper to stop and join the thread.
        // This must happen BEFORE WAL shutdown, because close_session_internal()
        // writes to the WAL.
        self.shutdown_sweeper.store(true, Ordering::Release);
        let sweeper_handle = {
            let mut guard = match self.sweeper_thread.lock() {
                Ok(g) => g,
                Err(poisoned) => poisoned.into_inner(),
            };
            guard.take()
        };
        if let Some(handle) = sweeper_handle {
            let _ = handle.join();
        }

        // M7p2: Final sweep for sessions that expired since the last scan.
        self.sweep_expired_sessions();

        // M7p2: Clean up remaining active sessions in-memory only.
        // We intentionally do NOT write Close WAL events here so that sessions
        // survive an unclean shutdown and are restored on the next open.
        // Agents that forget to call close_session() will have their sessions
        // replayed from WAL and restored as active on restart.
        let remaining: Vec<(crate::session::SessionId, String)> = self
            .sessions
            .iter()
            .map(|e| (*e.key(), e.value().agent_id.as_str().to_owned()))
            .collect();
        for (session_id, agent_id) in remaining {
            self.sessions.remove(&session_id);
            self.rate_limiter
                .remove(agent_id.as_str(), session_id.as_u64());
            tracing::debug!(
                session_id = %session_id,
                "shutdown: in-memory session cleanup (no Close WAL event)"
            );
        }

        // Stop the checkpoint thread before the final checkpoint to avoid races.
        self.shutdown_checkpoint.store(true, Ordering::Release);
        let checkpoint_handle = {
            let mut guard = match self.checkpoint_thread.lock() {
                Ok(g) => g,
                Err(poisoned) => poisoned.into_inner(),
            };
            guard.take()
        };
        if let Some(handle) = checkpoint_handle {
            // Distinguish a panicked checkpoint thread from a clean exit so a
            // dead durability thread is observable rather than silently
            // swallowed (CONCURRENCY-2). The supervisor closure already latched
            // `checkpoint_thread_died` on panic; we additionally log here so the
            // shutdown path is self-explaining. (A bare-thread panic that
            // escaped `catch_unwind` would still be caught by `join()` here.)
            if handle.join().is_err() {
                self.metrics
                    .checkpoint_thread_died
                    .store(true, Ordering::Release);
                tracing::error!(
                    "checkpoint thread had panicked before shutdown; final \
                     checkpoint below covers any signals it missed"
                );
            }
        }

        // Shut down both text syncers (item + creator).
        shutdown_text_syncer(&self.text_tx, &self.text_syncer_thread, "text");
        shutdown_text_syncer(
            &self.creator_text_tx,
            &self.creator_text_syncer_thread,
            "creator text",
        );

        // Checkpoint signal state to storage.
        // Ordering: stop checkpoint thread -> checkpoint ledger -> shutdown WAL.
        //
        // CONCURRENCY-3: the `closed` flag (CAS'd true above) is the shutdown
        // latch — it makes this method idempotent and flips health to degraded.
        // It does NOT itself fence concurrent writers: callers must stop issuing
        // writes before `close()`. Any signal events that landed in the WAL
        // after this final checkpoint's `wal_sequence` are re-applied by WAL
        // replay on the next `open()`, so a late writer is not lost — it is
        // simply recovered rather than checkpointed here.
        //
        // SHUTDOWN-2: a failed final checkpoint/flush previously only logged,
        // so an explicit `close()` returned `Ok(())` and the process exited 0
        // on lost durable state. We now capture the first such error and
        // surface it from `shutdown_inner` (and therefore `close()`), while
        // still attempting every remaining flush step so the disk is left as
        // consistent as possible.
        let mut first_err: Option<TidalError> = None;
        if let (Some(ledger), Some(storage)) = (&self.ledger, &self.storage) {
            let meta = crate::signals::checkpoint::CheckpointMeta {
                checkpoint_time_ns: Timestamp::now().as_nanos(),
                // Acquire pairs with the Release in WalHandleWriter::append_signal,
                // ensuring we see the highest seq committed by any WAL writer thread.
                wal_sequence: self.last_wal_seq.load(Ordering::Acquire),
                payload_hash: [0u8; 32], // computed by checkpoint()
            };
            if let Err(e) = ledger.checkpoint(storage.items_engine(), meta) {
                tracing::error!(error = %e, "signal ledger checkpoint failed during shutdown");
                first_err.get_or_insert(e);
            }
            // Checkpoint the secondary derived ledgers (cohort M6, co-engagement
            // M6, community M9p3 — whose per-contributor entries cannot be rebuilt
            // from the WAL — and preference vectors) through the ONE shared helper
            // so the shutdown, backup, and periodic paths cannot drift. Shutdown
            // must surface a failure, so we fold the helper's first error into
            // `first_err` (backup/periodic discard it instead).
            if let Some(e) = crate::db::state_rebuild::checkpoint_secondary_ledgers(
                storage.items_engine(),
                Some((self.cohort_ledger.as_ref(), meta)),
                self.community_ledger.as_ref(),
                self.co_engagement.as_ref(),
                self.preference_vectors.as_ref(),
            ) {
                first_err.get_or_insert(e);
            }
            // BLOCKER 6: persist the follower replication high-water-mark so a
            // clean restart restores its idempotency boundary instead of
            // resetting to 0. The receiver thread was already joined at the top
            // of this method, so no apply can advance the HWM past what we write
            // here. Non-fatal: a failure is captured (surfaced from close()) but
            // the next open simply restores an older, still-correct boundary.
            if let Err(e) = crate::db::state_rebuild::checkpoint_replication_state(
                storage.items_engine(),
                self.replication_state.as_ref(),
            ) {
                tracing::error!(error = %e, "replication-state checkpoint failed during shutdown");
                first_err.get_or_insert(e);
            }
            if let Err(e) = storage.flush() {
                tracing::error!(error = %e, "storage flush failed during shutdown");
                first_err.get_or_insert(e);
            }
        }

        // 2. Shut down WAL (needs ownership via take()).
        //
        // W4: recover from a poisoned WAL mutex instead of `?`-returning. A
        // panic in a prior WAL access poisons the lock, but the inner Option is
        // still structurally valid (the panic was in a writer, not mid-mutation
        // of this Option), so `into_inner()` hands us the WAL handle to shut down
        // cleanly. Early-returning here would (a) skip the final WAL shutdown and
        // checkpoint marker on the one path that most needs them, and (b) discard
        // the `first_err` already captured from the durable-flush steps above —
        // masking a real durability failure behind a generic "mutex poisoned".
        let mut wal_guard = match self.wal.lock() {
            Ok(g) => g,
            Err(poisoned) => {
                tracing::error!(
                    "WAL mutex was poisoned by an earlier panic; recovering the \
                     guard to still attempt final WAL shutdown"
                );
                poisoned.into_inner()
            }
        };
        let wal_opt = wal_guard.take();
        drop(wal_guard);

        if let Some(wal) = wal_opt {
            let seq = self.last_wal_seq.load(Ordering::Acquire);
            if seq > 0 {
                // Write WAL checkpoint marker so the next open knows the replay
                // start point. A failed marker means the next open replays from
                // an older marker (correct, just slower), but it still signals a
                // failing disk — surface it (SHUTDOWN-2).
                if let Err(e) = wal.checkpoint(seq) {
                    tracing::error!(error = %e, "WAL checkpoint marker failed during shutdown");
                    first_err.get_or_insert_with(|| {
                        TidalError::Durability(DurabilityError {
                            message: format!("WAL checkpoint marker failed during shutdown: {e}"),
                        })
                    });
                }
            }
            wal.shutdown().map_err(|e| {
                TidalError::Durability(DurabilityError {
                    message: format!("WAL shutdown failed: {e}"),
                })
            })?;

            // Compact WAL segments that are covered by the checkpoint.
            // Runs AFTER wal.shutdown() to avoid racing with the writer thread.
            // Compaction failure is non-fatal: segments survive and are replayed
            // redundantly on next open.
            if seq > 0
                && let Some(wal_dir) = self.config.resolved_wal_dir()
                && let Err(e) = crate::wal::compaction::compact_wal(&wal_dir, seq)
            {
                tracing::warn!(error = %e, "WAL compaction failed during shutdown");
            }
        }

        // SHUTDOWN-2: surface the first durable-flush failure so `close()`
        // returns it (the process must NOT exit 0 on a failed final flush).
        // `Drop` logs it instead — see the `Drop` impl below.
        first_err.map_or(Ok(()), Err)
    }
}

impl Drop for TidalDb {
    fn drop(&mut self) {
        // Stop metrics HTTP server if still running.
        #[cfg(feature = "metrics")]
        if let Some(ref mut handle) = self.metrics_handle {
            handle.stop();
        }

        // SHUTDOWN-2: `Drop` cannot return, so a failed final flush is logged
        // at error level here (the explicit `close()` path returns it instead).
        // Callers that need to observe shutdown durability failures must call
        // `close()` rather than relying on `Drop`.
        if let Err(e) = self.shutdown_inner() {
            tracing::error!(error = %e, "error during tidaldb shutdown (final flush failed)");
        }
    }
}

impl std::fmt::Debug for TidalDb {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TidalDb")
            .field("mode", &self.config.mode)
            .field("closed", &self.closed.load(Ordering::Relaxed))
            .finish_non_exhaustive()
    }
}

#[cfg(test)]
mod tests {
    use crate::TidalDb;

    /// W4: a WAL mutex poisoned by an earlier panic must not make
    /// `shutdown_inner` early-return a generic "mutex poisoned" error. The guard
    /// is recovered via `into_inner()` so the final WAL shutdown still runs and
    /// any durable-flush failure captured earlier is what surfaces — not the
    /// poison.
    #[test]
    fn shutdown_recovers_from_poisoned_wal_mutex() {
        // Driving shutdown explicitly below; the later `Drop` is a harmless
        // idempotent no-op (the `closed` CAS short-circuits the second pass).
        let db = TidalDb::builder().ephemeral().open().expect("open");

        // Poison the WAL mutex by panicking while its guard is held.
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let _guard = db.wal.lock().expect("lock");
            panic!("poison the WAL mutex");
        }));
        assert!(result.is_err(), "the helper closure must have panicked");
        assert!(
            db.wal.lock().is_err(),
            "WAL mutex must now be poisoned for the test to be meaningful"
        );

        // shutdown_inner must recover the poisoned guard and return Ok (ephemeral
        // mode has no durable-flush failures to surface).
        db.shutdown_inner()
            .expect("shutdown must recover from a poisoned WAL mutex, not error on it");
    }
}
