use std::{fs, path::Path};

use super::error::WalError;

/// File name for the checkpoint metadata.
const CHECKPOINT_FILE: &str = "checkpoint.meta";

/// Temporary file used for atomic write.
const CHECKPOINT_TMP: &str = "checkpoint.meta.tmp";

/// Manages checkpoint metadata for the WAL.
///
/// A checkpoint marks the sequence number through which all signal events
/// have been materialized to durable storage. On recovery, the WAL only
/// needs to replay events after the checkpoint.
///
/// Checkpoint writes are atomic: write to a temp file, fsync, then rename.
pub struct CheckpointManager;

impl CheckpointManager {
    /// Write a checkpoint with the given sequence number and timestamp.
    ///
    /// Uses write-to-temp-then-rename for atomicity on POSIX systems.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` on filesystem failure.
    pub fn write(dir: &Path, seq: u64, ts: u64) -> Result<(), WalError> {
        let content = format!("seq={seq}\nts={ts}\n");
        let tmp_path = dir.join(CHECKPOINT_TMP);
        let final_path = dir.join(CHECKPOINT_FILE);

        // Remove any temp left behind by a crash between a prior write and its
        // rename. The deterministic temp name means `fs::write` below would
        // overwrite it anyway, but unlinking first keeps the partial bytes of a
        // failed prior attempt from ever being observed and makes the leak
        // impossible to accumulate across repeated write failures.
        Self::remove_stale_temp(&tmp_path)?;

        fs::write(&tmp_path, content.as_bytes())?;

        // fsync the temp file to ensure contents are durable before rename. On
        // macOS a plain fsync does not flush the device write cache, so route
        // through the F_FULLFSYNC-backed helper — the checkpoint is a durable
        // artifact and must meet the same crash-durability bar as the WAL
        // segments it gates compaction of.
        let file = fs::File::open(&tmp_path)?;
        super::sync_file_durable(&file)?;
        drop(file);

        // Atomic rename (POSIX guarantees)
        fs::rename(&tmp_path, &final_path)?;

        // Fsync the directory to ensure the rename (directory entry update)
        // is durable. Without this, a crash after rename but before the
        // directory metadata is flushed could lose the checkpoint file. On
        // macOS this routes through the F_FULLFSYNC-backed helper.
        super::sync_dir_durable(dir)?;

        Ok(())
    }

    /// Remove a stale checkpoint temp file if one is present.
    ///
    /// A crash between [`write`](Self::write)'s temp write and its rename leaves
    /// `checkpoint.meta.tmp` on disk. Those bytes are never read by
    /// [`read`](Self::read) (which only opens the final `checkpoint.meta`), so a
    /// stale temp is harmless to correctness — but it should not linger. A
    /// `NotFound` is the common, non-error case (no crash happened).
    fn remove_stale_temp(tmp_path: &Path) -> Result<(), WalError> {
        match fs::remove_file(tmp_path) {
            Ok(()) => Ok(()),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
            Err(e) => Err(WalError::Io(e)),
        }
    }

    /// Sweep an orphaned checkpoint temp file (`checkpoint.meta.tmp`) left by a
    /// crash between [`write`](Self::write)'s temp write and its rename.
    ///
    /// Call ONLY from a writable open/recovery path (the engine owns and writes
    /// to the WAL dir there). It is deliberately NOT called from
    /// [`read`](Self::read): a pure read must not mutate disk, or it would error
    /// on a read-only mount (forensic inspection) and could unlink the temp out
    /// from under a concurrent writer's rename. The orphan is harmless until
    /// swept ([`read`](Self::read) only ever opens the final file), and
    /// [`write`](Self::write) also pre-sweeps, so the temp can never accumulate on
    /// an actively-written WAL.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` if an existing temp cannot be removed.
    pub(crate) fn sweep_stale_temp(dir: &Path) -> Result<(), WalError> {
        Self::remove_stale_temp(&dir.join(CHECKPOINT_TMP))
    }

    /// Read the checkpoint metadata.
    ///
    /// Returns `None` if the checkpoint file does not exist (fresh WAL).
    /// Returns `Some((seq, ts))` on success.
    ///
    /// This is a pure, side-effect-free read: it never mutates the directory.
    /// That keeps it safe against a read-only mount (forensic inspection via
    /// `tidalctl`) and unable to race a concurrent writer's checkpoint rename.
    /// Sweeping an orphaned temp file happens on the writable open path instead —
    /// see [`sweep_stale_temp`](Self::sweep_stale_temp), called from WAL recovery.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` on read failure, or `WalError::Corruption`
    /// if the file exists but cannot be parsed.
    pub fn read(dir: &Path) -> Result<Option<(u64, u64)>, WalError> {
        let path = dir.join(CHECKPOINT_FILE);
        let content = match fs::read_to_string(&path) {
            Ok(c) => c,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(e) => return Err(WalError::Io(e)),
        };

        let mut seq: Option<u64> = None;
        let mut ts: Option<u64> = None;

        for line in content.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            if let Some(val) = line.strip_prefix("seq=") {
                seq = Some(val.parse::<u64>().map_err(|_| WalError::Corruption {
                    message: format!("invalid seq value in checkpoint: '{val}'"),
                })?);
            } else if let Some(val) = line.strip_prefix("ts=") {
                ts = Some(val.parse::<u64>().map_err(|_| WalError::Corruption {
                    message: format!("invalid ts value in checkpoint: '{val}'"),
                })?);
            }
        }

        match (seq, ts) {
            (Some(s), Some(t)) => Ok(Some((s, t))),
            _ => Err(WalError::Corruption {
                message: "checkpoint file missing seq or ts field".into(),
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn write_and_read_roundtrip() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        CheckpointManager::write(dir.path(), 1000, 2_000_000_000).expect("write should succeed");
        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, Some((1000, 2_000_000_000)));
    }

    #[test]
    fn read_missing_returns_none() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, None);
    }

    #[test]
    fn overwrite_updates_values() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        CheckpointManager::write(dir.path(), 100, 200).expect("write should succeed");
        CheckpointManager::write(dir.path(), 500, 600).expect("write should succeed");
        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, Some((500, 600)));
    }

    #[test]
    fn corrupt_file_returns_error() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let path = dir.path().join("checkpoint.meta");
        fs::write(&path, "garbage data").expect("write should succeed");
        let result = CheckpointManager::read(dir.path());
        assert!(result.is_err());
    }

    #[test]
    fn partial_file_returns_error() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let path = dir.path().join("checkpoint.meta");
        fs::write(&path, "seq=100\n").expect("write should succeed"); // missing ts
        let result = CheckpointManager::read(dir.path());
        assert!(result.is_err());
    }

    #[test]
    fn max_u64_values() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        CheckpointManager::write(dir.path(), u64::MAX, u64::MAX).expect("write should succeed");
        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, Some((u64::MAX, u64::MAX)));
    }

    #[test]
    fn zero_values() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        CheckpointManager::write(dir.path(), 0, 0).expect("write should succeed");
        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, Some((0, 0)));
    }

    #[test]
    fn read_is_side_effect_free_and_leaves_temp() {
        // Simulate a crash between write() and rename: a temp file lingers with
        // no final checkpoint. read() must report None (no checkpoint) and must
        // NOT mutate the directory — it must leave the temp in place so it stays
        // safe against a read-only mount and cannot race a concurrent writer's
        // rename. (The temp is swept on the writable open path instead; see
        // `sweep_stale_temp`.)
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let tmp = dir.path().join(CHECKPOINT_TMP);
        fs::write(&tmp, b"seq=42\nts=99\n").expect("temp write should succeed");

        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, None, "a lone temp is not a committed checkpoint");
        assert!(
            tmp.exists(),
            "read must NOT mutate the directory (no temp sweep)"
        );
    }

    #[test]
    fn sweep_stale_temp_removes_orphaned_temp() {
        // The writable open/recovery path sweeps the orphan that read() leaves.
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let tmp = dir.path().join(CHECKPOINT_TMP);
        fs::write(&tmp, b"seq=42\nts=99\n").expect("temp write should succeed");

        CheckpointManager::sweep_stale_temp(dir.path()).expect("sweep should succeed");
        assert!(
            !tmp.exists(),
            "sweep_stale_temp must remove the orphaned temp"
        );
        // Idempotent: a second sweep with no temp present is a clean no-op.
        CheckpointManager::sweep_stale_temp(dir.path()).expect("sweep of absent temp is a no-op");
    }

    #[test]
    fn write_clears_stale_temp_and_succeeds() {
        // A stale temp from a prior failed write must not corrupt the next write:
        // the new checkpoint commits cleanly and no temp is left behind.
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let tmp = dir.path().join(CHECKPOINT_TMP);
        fs::write(&tmp, b"partial garbage from a crash").expect("temp write should succeed");

        CheckpointManager::write(dir.path(), 7, 8).expect("write should succeed");
        let result = CheckpointManager::read(dir.path()).expect("read should succeed");
        assert_eq!(result, Some((7, 8)));
        assert!(!tmp.exists(), "write must not leave a temp file behind");
    }

    mod proptests {
        use proptest::prelude::*;

        use super::*;

        proptest! {
            #[test]
            fn roundtrip(seq: u64, ts: u64) {
                let dir = tempfile::tempdir().expect("tempdir creation should succeed");
                CheckpointManager::write(dir.path(), seq, ts)?;
                let result = CheckpointManager::read(dir.path())?;
                prop_assert_eq!(result, Some((seq, ts)));
            }
        }
    }
}
