//! Checkpoint persistence for resumable copy operations.
//!
//! Writes completed source keys to a newline-delimited file.
//! On resume, loads the set of already-completed keys to skip.

use std::collections::HashSet;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::sync::Mutex;

use tracing::info;

use crate::error::Result;

/// Manages checkpoint state for a copy session.
#[derive(Debug)]
pub struct Checkpoint {
    /// Keys that were already completed (loaded on resume).
    completed: HashSet<String>,
    /// File writer for appending newly completed keys.
    writer: Option<Mutex<std::io::BufWriter<std::fs::File>>>,
    /// Counter for fsync batching.
    writes_since_sync: Mutex<u64>,
    /// Fsync every N writes.
    sync_interval: u64,
}

impl Checkpoint {
    /// Create a new checkpoint that writes to the given path.
    ///
    /// If `resume` is true and the file exists, loads previously completed keys.
    pub fn new(path: &Path, resume: bool) -> Result<Self> {
        let mut completed = HashSet::new();

        if resume && path.exists() {
            let file = std::fs::File::open(path)?;
            let reader = BufReader::new(file);
            for line in reader.lines() {
                let key = line?;
                if !key.is_empty() {
                    completed.insert(key);
                }
            }
            info!(
                count = completed.len(),
                path = %path.display(),
                "loaded checkpoint"
            );
        }

        let file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)?;

        Ok(Self {
            completed,
            writer: Some(Mutex::new(std::io::BufWriter::new(file))),
            writes_since_sync: Mutex::new(0),
            sync_interval: 100,
        })
    }

    /// Create a no-op checkpoint (when no checkpoint path is configured).
    pub fn noop() -> Self {
        Self {
            completed: HashSet::new(),
            writer: None,
            writes_since_sync: Mutex::new(0),
            sync_interval: 100,
        }
    }

    /// Returns `true` if this key was already completed in a previous session.
    pub fn is_completed(&self, key: &str) -> bool {
        self.completed.contains(key)
    }

    /// Record a key as completed. Writes to the checkpoint file.
    pub fn mark_completed(&self, key: &str) -> Result<()> {
        if let Some(ref writer) = self.writer {
            let mut w = writer.lock().unwrap();
            writeln!(w, "{key}")?;

            let mut count = self.writes_since_sync.lock().unwrap();
            *count += 1;
            if *count >= self.sync_interval {
                w.flush()?;
                *count = 0;
            }
        }
        Ok(())
    }

    /// Flush any buffered writes.
    pub fn flush(&self) -> Result<()> {
        if let Some(ref writer) = self.writer {
            let mut w = writer.lock().unwrap();
            w.flush()?;
        }
        Ok(())
    }
}

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

    #[test]
    fn write_and_resume() {
        let dir = std::env::temp_dir().join("s3bolt_test_ckpt");
        let path = dir.join("test.ckpt");
        let _ = std::fs::create_dir_all(&dir);
        let _ = std::fs::remove_file(&path);

        // Write some keys.
        {
            let ckpt = Checkpoint::new(&path, false).unwrap();
            ckpt.mark_completed("key1").unwrap();
            ckpt.mark_completed("key2").unwrap();
            ckpt.flush().unwrap();
        }

        // Resume and verify.
        {
            let ckpt = Checkpoint::new(&path, true).unwrap();
            assert!(ckpt.is_completed("key1"));
            assert!(ckpt.is_completed("key2"));
            assert!(!ckpt.is_completed("key3"));
        }

        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn noop_checkpoint() {
        let ckpt = Checkpoint::noop();
        assert!(!ckpt.is_completed("anything"));
        ckpt.mark_completed("key").unwrap(); // should not panic
    }
}
