//! Lock-free WAL-state inspection shared by the `status` and `diagnostics`
//! commands.
//!
//! [`gather_wal_state`] is the standalone path used by `status`: it lists
//! segments, reads the checkpoint, and sizes the directory directly.
//! [`WalState::from_report`] is the zero-extra-IO path used by `diagnostics`,
//! which already holds a full [`WalDiagnosticReport`] and must not re-scan the
//! directory or re-read the checkpoint.

use serde::Serialize;

use tidaldb::wal::diagnostics::WalDiagnosticReport;

/// Read-only snapshot of a WAL directory's segment + checkpoint state.
#[derive(Serialize)]
pub(crate) struct WalState {
    pub(crate) segments: usize,
    pub(crate) first_seq: u64,
    pub(crate) last_segment_seq: u64,
    pub(crate) checkpoint_seq: u64,
    pub(crate) checkpoint_ts: u64,
    pub(crate) wal_dir_bytes: u64,
}

impl WalState {
    /// An empty WAL (no segments, no checkpoint, zero bytes).
    pub(crate) const fn empty() -> Self {
        Self {
            segments: 0,
            first_seq: 0,
            last_segment_seq: 0,
            checkpoint_seq: 0,
            checkpoint_ts: 0,
            wal_dir_bytes: 0,
        }
    }

    /// Project an already-computed [`WalDiagnosticReport`] into a [`WalState`].
    ///
    /// `diagnostics` runs [`diagnose_wal`](tidaldb::wal::diagnostics::diagnose_wal)
    /// once and reuses its output here, so the segment scan and checkpoint read
    /// happen exactly one time per invocation. `last_segment_seq` mirrors the
    /// standalone [`gather_wal_state`] semantics: the `first_seq` of the last
    /// segment file (the legacy "last segment sequence", not the last event
    /// sequence). `wal_dir_bytes` uses the report's total segment bytes; unlike
    /// [`dir_size`] this excludes the checkpoint/session-journal sidecar files,
    /// which the diagnostics surface accounts for separately.
    pub(crate) fn from_report(report: &WalDiagnosticReport) -> Self {
        let first_seq = report.segments.first().map_or(0, |s| s.first_seq);
        let last_segment_seq = report.segments.last().map_or(0, |s| s.first_seq);
        Self {
            segments: report.segment_count,
            first_seq,
            last_segment_seq,
            checkpoint_seq: report.checkpoint_seq,
            checkpoint_ts: report.checkpoint_ts,
            wal_dir_bytes: report.total_segment_bytes,
        }
    }
}

/// Gather WAL state by directly scanning the directory (the `status` path).
///
/// Returns an empty [`WalState`] when the WAL directory is absent (a fresh
/// store is not an error). Returns `Err` when the directory exists but its
/// segments, checkpoint, or size could not be read — the caller maps that to a
/// degraded exit code.
pub(crate) fn gather_wal_state(wal_dir: &std::path::Path) -> Result<WalState, String> {
    if !wal_dir.exists() {
        return Ok(WalState::empty());
    }

    let segments = tidaldb::wal::segment::list_segments(wal_dir).map_err(|e| format!("{e}"))?;

    let first_seq = segments.first().map_or(0, |(seq, _)| *seq);
    let last_segment_seq = segments.last().map_or(0, |(seq, _)| *seq);

    let (checkpoint_seq, checkpoint_ts) =
        match tidaldb::wal::checkpoint::CheckpointManager::read(wal_dir) {
            Ok(Some((seq, ts))) => (seq, ts),
            Ok(None) => (0, 0),
            Err(e) => return Err(format!("checkpoint read error: {e}")),
        };

    let wal_dir_bytes = dir_size(wal_dir).map_err(|e| format!("{e}"))?;

    Ok(WalState {
        segments: segments.len(),
        first_seq,
        last_segment_seq,
        checkpoint_seq,
        checkpoint_ts,
        wal_dir_bytes,
    })
}

/// Sum the byte size of every regular file directly in `dir` (non-recursive).
pub(crate) fn dir_size(dir: &std::path::Path) -> Result<u64, std::io::Error> {
    let mut total = 0u64;
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let meta = entry.metadata()?;
        if meta.is_file() {
            total += meta.len();
        }
    }
    Ok(total)
}
