//! Durable commit-layer snapshots for incremental substrate recovery (bl-015).
//!
//! `Substrate::open` rebuilds the in-memory commit layer by replaying the
//! whole oplog. A snapshot persists the merkle cells plus the `(timestamp,
//! op_id)` boundary of the last op folded into them, so a restart restores
//! the cells and replays only operations appended after the boundary —
//! recovery cost becomes proportional to ops-since-snapshot, not history.
//!
//! It also lets an offline verifier read a historical `state_root` directly
//! from the snapshot file without touching the oplog.

use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;

use ontoref_commit::{CellKey, StateRoot};
use ontoref_types::canonical;
use ontoref_types::{Hlc, OpId};
use serde::{Deserialize, Serialize};

use crate::error::{Error, Result};

/// A persisted commit-layer checkpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
    /// `(timestamp, op_id)` of the last operation folded into this
    /// snapshot, in the oplog's total HLC order. `None` for a snapshot of
    /// an empty substrate.
    pub boundary: Option<(Hlc, OpId)>,
    /// Latest value per cell at the boundary.
    pub cells: BTreeMap<CellKey, Vec<u8>>,
    /// State root the cells hash to — the value an offline verifier reads
    /// directly, without replaying the oplog.
    pub state_root: StateRoot,
}

impl Snapshot {
    /// Load a snapshot from `path`. Returns `Ok(None)` when the file is
    /// absent (no snapshot taken yet → the caller does a full replay).
    ///
    /// # Errors
    ///
    /// [`Error::Io`] on a read failure other than not-found;
    /// [`Error::Canonical`] when the bytes do not decode.
    pub fn load(path: &Path) -> Result<Option<Self>> {
        match std::fs::read(path) {
            Ok(bytes) => Ok(Some(canonical::decode(&bytes)?)),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(Error::Io(e)),
        }
    }

    /// Persist atomically: write to a sibling temp file, `fsync` it, then
    /// rename over `path`. A crash therefore leaves either the old snapshot
    /// or the complete new one — never a torn file.
    ///
    /// # Errors
    ///
    /// [`Error::Canonical`] on encode failure; [`Error::Io`] on any IO step.
    pub fn write(&self, path: &Path) -> Result<()> {
        let bytes = canonical::encode(self)?;
        let parent = path
            .parent()
            .ok_or_else(|| Error::Io(std::io::Error::other("snapshot path has no parent")))?;
        let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
        tmp.write_all(&bytes)?;
        tmp.as_file().sync_all()?;
        tmp.persist(path).map_err(|e| Error::Io(e.error))?;
        Ok(())
    }
}
