use std::collections::HashSet;
use std::io::{self, Read};
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::blockstore::{
    BlockOffset, BlockstoreSnapshot, CommitEpoch, CommitError, DataFileId, QuiesceGuard,
    RebuildError, TranquilBlockStore,
};
use crate::clock::SystemClock;
use crate::eventlog::{
    EventLog, EventLogConfig, EventLogFreezeGuard, EventLogSnapshotState, EventSequence,
    EventWithMutations, SegmentId, SegmentOffset,
};
use crate::io::{RealIO, StorageIO};
use crate::metastore::event_keys::{did_events_key, metastore_cursor_key, rev_to_seq_key};
use crate::metastore::keys::UserHash;
use crate::metastore::partitions::Partition;
use crate::metastore::recovery::{CommitMutationSet, replay_mutation_set};
use crate::metastore::repo_meta::{RepoMetaValue, RepoStatus, repo_meta_key};
use crate::metastore::{Metastore, MetastoreConfig, MetastoreError};

const BACKUP_FORMAT_VERSION: u32 = 1;
const MANIFEST_FILENAME: &str = "backup.manifest";
const BLOCKS_DIR: &str = "blocks";
const EVENTS_DIR: &str = "events";
const METASTORE_DIR: &str = "metastore";
const INDEX_DIR: &str = "block_index";
const LOCK_FILE_NAME: &str = ".lock";

#[derive(Debug, thiserror::Error)]
pub enum BackupError {
    #[error("backup io: {0}")]
    Io(#[from] io::Error),
    #[error("blockstore quiesce failed: {0}")]
    Quiesce(#[from] CommitError),
    #[error("metastore persist failed: {0}")]
    Metastore(#[from] MetastoreError),
    #[error("index error: {0}")]
    Index(io::Error),
    #[error("index rebuild failed: {0}")]
    IndexRebuild(#[from] RebuildError),
    #[error("checksum mismatch for {path}: expected {expected:#018x}, got {actual:#018x}")]
    ChecksumMismatch {
        path: String,
        expected: u64,
        actual: u64,
    },
    #[error("size mismatch for {path}: expected {expected} bytes, got {actual}")]
    SizeMismatch {
        path: String,
        expected: u64,
        actual: u64,
    },
    #[error("missing file in backup: {0}")]
    MissingFile(String),
    #[error("restore verification failed: {0}")]
    RestoreVerification(String),
    #[error("target directory not empty: {0}")]
    TargetNotEmpty(String),
    #[error("incremental chain mismatch: {0}")]
    ChainMismatch(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MetastoreSeqno(u64);

impl MetastoreSeqno {
    pub fn new(seqno: u64) -> Self {
        Self(seqno)
    }

    pub fn raw(self) -> u64 {
        self.0
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackupKind {
    #[default]
    Full,
    Incremental,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupManifest {
    pub version: u32,
    pub created_at_ms: u64,
    pub blockstore: BlockstoreManifest,
    pub eventlog: EventLogManifest,
    pub metastore_seqno: MetastoreSeqno,
    pub files: Vec<BackupFileEntry>,
    #[serde(default)]
    pub kind: BackupKind,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub base_blockstore: Option<BlockstoreManifest>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub base_eventlog: Option<EventLogManifest>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockstoreManifest {
    pub write_cursor_file_id: DataFileId,
    pub write_cursor_offset: BlockOffset,
    pub epoch: CommitEpoch,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub shard_cursors: Vec<ShardCursorEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShardCursorEntry {
    pub file_id: DataFileId,
    pub offset: BlockOffset,
}

impl BlockstoreManifest {
    pub fn min_active_file_id(&self) -> DataFileId {
        match self.shard_cursors.is_empty() {
            true => self.write_cursor_file_id,
            false => self
                .shard_cursors
                .iter()
                .map(|c| c.file_id)
                .min()
                .unwrap_or(self.write_cursor_file_id),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventLogManifest {
    pub max_seq: EventSequence,
    pub active_segment_id: SegmentId,
    pub active_segment_position: SegmentOffset,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupFileEntry {
    pub path: String,
    pub size: u64,
    pub xxh3_checksum: u64,
}

struct ConsistentSnapshot {
    blockstore: BlockstoreSnapshot,
    eventlog: EventLogSnapshotState,
    metastore_seqno: MetastoreSeqno,
    metastore_files: Vec<BackupFileEntry>,
    quiesce_guard: QuiesceGuard,
    _eventlog_guard: EventLogFreezeGuard,
}

enum BackupLineage<'a> {
    Full,
    Incremental { base: &'a BackupManifest },
}

pub struct BackupCoordinator<'a, S: StorageIO> {
    blockstore: &'a TranquilBlockStore<RealIO, SystemClock>,
    eventlog: &'a EventLog<S>,
    metastore: &'a Metastore,
}

impl<'a, S: StorageIO + Send + Sync + 'static> BackupCoordinator<'a, S> {
    pub fn new(
        blockstore: &'a TranquilBlockStore<RealIO, SystemClock>,
        eventlog: &'a EventLog<S>,
        metastore: &'a Metastore,
    ) -> Self {
        Self {
            blockstore,
            eventlog,
            metastore,
        }
    }

    pub fn create_backup(&self, destination: &Path) -> Result<BackupManifest, BackupError> {
        std::fs::create_dir_all(destination)?;

        let ConsistentSnapshot {
            blockstore: bs,
            eventlog: el,
            metastore_seqno,
            metastore_files,
            quiesce_guard,
            _eventlog_guard: eventlog_guard,
        } = self.take_consistent_snapshot(destination)?;

        let mut files = metastore_files;

        copy_blockstore_files(
            &bs,
            DataFileId::new(0),
            self.blockstore.data_dir(),
            &destination.join(BLOCKS_DIR),
            &mut files,
        )?;

        copy_eventlog_files(
            &el,
            SegmentId::new(0),
            self.eventlog.segments_dir(),
            &destination.join(EVENTS_DIR),
            &mut files,
        )?;

        quiesce_guard.resume();
        drop(eventlog_guard);

        let manifest = self.build_manifest(bs, el, metastore_seqno, files, BackupLineage::Full);

        write_manifest(&manifest, destination)?;

        Ok(manifest)
    }

    pub fn create_incremental_backup(
        &self,
        base: &BackupManifest,
        destination: &Path,
    ) -> Result<BackupManifest, BackupError> {
        std::fs::create_dir_all(destination)?;

        let ConsistentSnapshot {
            blockstore: bs,
            eventlog: el,
            metastore_seqno,
            metastore_files,
            quiesce_guard,
            _eventlog_guard: eventlog_guard,
        } = self.take_consistent_snapshot(destination)?;

        let mut files = metastore_files;

        copy_blockstore_files(
            &bs,
            base.blockstore.min_active_file_id(),
            self.blockstore.data_dir(),
            &destination.join(BLOCKS_DIR),
            &mut files,
        )?;

        copy_eventlog_files(
            &el,
            base.eventlog.active_segment_id,
            self.eventlog.segments_dir(),
            &destination.join(EVENTS_DIR),
            &mut files,
        )?;

        quiesce_guard.resume();
        drop(eventlog_guard);

        let manifest = self.build_manifest(
            bs,
            el,
            metastore_seqno,
            files,
            BackupLineage::Incremental { base },
        );

        write_manifest(&manifest, destination)?;

        Ok(manifest)
    }

    fn take_consistent_snapshot(
        &self,
        destination: &Path,
    ) -> Result<ConsistentSnapshot, BackupError> {
        let (bs_snapshot, quiesce_guard) = self.blockstore.quiesce()?;

        let (el_snapshot, eventlog_guard) = self.eventlog.freeze()?;

        self.metastore.persist()?;
        let metastore_seqno = MetastoreSeqno::new(self.metastore.database().seqno());

        let mut metastore_files = Vec::new();
        copy_metastore_files(
            self.metastore.path(),
            &destination.join(METASTORE_DIR),
            &mut metastore_files,
        )?;

        Ok(ConsistentSnapshot {
            blockstore: bs_snapshot,
            eventlog: el_snapshot,
            metastore_seqno,
            metastore_files,
            quiesce_guard,
            _eventlog_guard: eventlog_guard,
        })
    }

    fn build_manifest(
        &self,
        bs: BlockstoreSnapshot,
        el: EventLogSnapshotState,
        metastore_seqno: MetastoreSeqno,
        files: Vec<BackupFileEntry>,
        lineage: BackupLineage<'_>,
    ) -> BackupManifest {
        let (kind, base_blockstore, base_eventlog) = match lineage {
            BackupLineage::Full => (BackupKind::Full, None, None),
            BackupLineage::Incremental { base } => (
                BackupKind::Incremental,
                Some(base.blockstore.clone()),
                Some(base.eventlog.clone()),
            ),
        };

        BackupManifest {
            version: BACKUP_FORMAT_VERSION,
            created_at_ms: crate::blockstore::WallClockMs::now().raw(),
            blockstore: {
                let max_cursor = bs
                    .shard_cursors
                    .iter()
                    .max_by_key(|c| (c.file_id, c.offset))
                    .copied();
                let shard_cursor_entries: Vec<ShardCursorEntry> = bs
                    .shard_cursors
                    .iter()
                    .map(|c| ShardCursorEntry {
                        file_id: c.file_id,
                        offset: c.offset,
                    })
                    .collect();
                BlockstoreManifest {
                    write_cursor_file_id: max_cursor
                        .map(|c| c.file_id)
                        .unwrap_or(DataFileId::new(0)),
                    write_cursor_offset: max_cursor
                        .map(|c| c.offset)
                        .unwrap_or(BlockOffset::new(0)),
                    epoch: bs.epoch,
                    shard_cursors: shard_cursor_entries,
                }
            },
            eventlog: EventLogManifest {
                max_seq: el.max_seq,
                active_segment_id: el.active_segment_id,
                active_segment_position: el.active_segment_position,
            },
            metastore_seqno,
            files,
            kind,
            base_blockstore,
            base_eventlog,
        }
    }
}

fn copy_blockstore_files(
    snapshot: &BlockstoreSnapshot,
    min_file_id: DataFileId,
    data_dir: &Path,
    dest_dir: &Path,
    files: &mut Vec<BackupFileEntry>,
) -> io::Result<()> {
    std::fs::create_dir_all(dest_dir)?;

    snapshot
        .data_files
        .iter()
        .filter(|&&fid| fid >= min_file_id)
        .try_for_each(|&file_id| {
            let src = data_dir.join(format!("{file_id}.tqb"));

            let max_bytes = snapshot
                .shard_cursors
                .iter()
                .find(|c| c.file_id == file_id)
                .map(|c| c.offset.raw());

            let dest = dest_dir.join(format!("{file_id}.tqb"));
            let size = copy_file_synced(&src, &dest, max_bytes)?;

            if let Some(expected) = max_bytes {
                verify_copy_size(size, expected, &src)?;
            }

            let checksum = checksum_file(&dest)?;

            files.push(BackupFileEntry {
                path: format!("{BLOCKS_DIR}/{file_id}.tqb"),
                size,
                xxh3_checksum: checksum,
            });

            Ok::<(), io::Error>(())
        })?;

    sync_dir(dest_dir)
}

fn copy_eventlog_files(
    snapshot: &EventLogSnapshotState,
    min_segment_id: SegmentId,
    segments_dir: &Path,
    dest_dir: &Path,
    files: &mut Vec<BackupFileEntry>,
) -> io::Result<()> {
    std::fs::create_dir_all(dest_dir)?;

    snapshot
        .sealed_segments
        .iter()
        .filter(|&&sid| sid >= min_segment_id)
        .try_for_each(|&seg_id| {
            let src = segments_dir.join(format!("{seg_id}.tqe"));
            let dest = dest_dir.join(format!("{seg_id}.tqe"));
            let size = copy_file_synced(&src, &dest, None)?;
            let checksum = checksum_file(&dest)?;

            files.push(BackupFileEntry {
                path: format!("{EVENTS_DIR}/{seg_id}.tqe"),
                size,
                xxh3_checksum: checksum,
            });

            Ok::<(), io::Error>(())
        })?;

    let active_src = segments_dir.join(format!("{}.tqe", snapshot.active_segment_id));
    let active_dest = dest_dir.join(format!("{}.tqe", snapshot.active_segment_id));
    let expected = snapshot.active_segment_position.raw();
    let size = copy_file_synced(&active_src, &active_dest, Some(expected))?;
    verify_copy_size(size, expected, &active_src)?;
    let checksum = checksum_file(&active_dest)?;

    files.push(BackupFileEntry {
        path: format!("{EVENTS_DIR}/{}.tqe", snapshot.active_segment_id),
        size,
        xxh3_checksum: checksum,
    });

    sync_dir(dest_dir)
}

fn copy_metastore_files(
    src: &Path,
    dest: &Path,
    files: &mut Vec<BackupFileEntry>,
) -> io::Result<()> {
    std::fs::create_dir_all(dest)?;
    copy_dir_recursive(src, dest, src, files)?;
    sync_dir(dest)
}

fn copy_dir_recursive(
    base: &Path,
    dest_base: &Path,
    current: &Path,
    files: &mut Vec<BackupFileEntry>,
) -> io::Result<()> {
    std::fs::read_dir(current)?.try_for_each(|entry| {
        let entry = entry?;
        let file_name = entry.file_name();
        let name = file_name.to_string_lossy();

        if name == LOCK_FILE_NAME {
            return Ok(());
        }

        let path = entry.path();
        let relative = path
            .strip_prefix(base)
            .map_err(|e| io::Error::other(e.to_string()))?;
        let dest_path = dest_base.join(relative);

        match entry.file_type()?.is_dir() {
            true => {
                std::fs::create_dir_all(&dest_path)?;
                copy_dir_recursive(base, dest_base, &path, files)
            }
            false => {
                let size = copy_file_synced(&path, &dest_path, None)?;
                let checksum = checksum_file(&dest_path)?;
                files.push(BackupFileEntry {
                    path: format!("{METASTORE_DIR}/{}", relative.display()),
                    size,
                    xxh3_checksum: checksum,
                });
                Ok(())
            }
        }
    })
}

fn copy_file_synced(src: &Path, dest: &Path, max_bytes: Option<u64>) -> io::Result<u64> {
    let src_file = std::fs::File::open(src)?;
    let mut dest_file = std::fs::File::create(dest)?;
    let total = match max_bytes {
        None => io::copy(&mut &src_file, &mut dest_file)?,
        Some(limit) => io::copy(&mut src_file.take(limit), &mut dest_file)?,
    };
    dest_file.sync_all()?;
    Ok(total)
}

fn verify_copy_size(actual: u64, expected: u64, src: &Path) -> io::Result<()> {
    (actual == expected).then_some(()).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::UnexpectedEof,
            format!("{}: expected {expected} bytes, got {actual}", src.display(),),
        )
    })
}

struct HashWriter(xxhash_rust::xxh3::Xxh3Default);

impl io::Write for HashWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.0.update(buf);
        Ok(buf.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

fn checksum_file(path: &Path) -> io::Result<u64> {
    let mut file = std::fs::File::open(path)?;
    let mut hasher = HashWriter(xxhash_rust::xxh3::Xxh3Default::new());
    io::copy(&mut file, &mut hasher)?;
    Ok(hasher.0.digest())
}

fn write_manifest(manifest: &BackupManifest, destination: &Path) -> io::Result<()> {
    let json = serde_json::to_string_pretty(manifest)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    let manifest_path = destination.join(MANIFEST_FILENAME);
    std::fs::write(&manifest_path, json.as_bytes())?;
    let f = std::fs::File::open(&manifest_path)?;
    f.sync_all()?;
    sync_dir(destination)?;
    Ok(())
}

pub fn read_manifest(backup_dir: &Path) -> io::Result<BackupManifest> {
    let data = std::fs::read(backup_dir.join(MANIFEST_FILENAME))?;
    let manifest: BackupManifest =
        serde_json::from_slice(&data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    if manifest.version != BACKUP_FORMAT_VERSION {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "backup format version {}, expected {BACKUP_FORMAT_VERSION}",
                manifest.version,
            ),
        ));
    }
    Ok(manifest)
}

#[derive(Debug)]
pub struct RestoreResult {
    pub blocks_files_restored: u32,
    pub event_segments_restored: u32,
    pub metastore_files_restored: u32,
}

#[derive(Debug)]
pub struct PitrResult {
    pub restore: RestoreResult,
    pub events_replayed: u64,
    pub target_seq: EventSequence,
}

#[derive(Debug)]
pub enum FileFailure {
    SizeMismatch(String),
    ChecksumMismatch(String),
    Unreadable(String, io::Error),
}

#[derive(Debug)]
pub struct VerifyResult {
    pub total_blocks: u64,
    pub total_events: u64,
    pub corrupted_blocks: u64,
    pub corrupted_events: u64,
    pub file_failures: Vec<FileFailure>,
}

impl VerifyResult {
    pub fn is_healthy(&self) -> bool {
        self.corrupted_blocks == 0 && self.corrupted_events == 0 && self.file_failures.is_empty()
    }
}

fn copy_validated_entry(
    entry: &BackupFileEntry,
    source_dir: &Path,
    target: &Path,
) -> Result<(), BackupError> {
    let src_path = source_dir.join(&entry.path);
    let dest_path = target.join(&entry.path);

    if let Some(parent) = dest_path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    let size = copy_file_synced(&src_path, &dest_path, None)?;

    if size != entry.size {
        return Err(BackupError::SizeMismatch {
            path: entry.path.clone(),
            expected: entry.size,
            actual: size,
        });
    }

    let actual_checksum = checksum_file(&dest_path)?;
    if actual_checksum != entry.xxh3_checksum {
        return Err(BackupError::ChecksumMismatch {
            path: entry.path.clone(),
            expected: entry.xxh3_checksum,
            actual: actual_checksum,
        });
    }

    Ok(())
}

struct RestoreDirs {
    blocks: std::path::PathBuf,
    events: std::path::PathBuf,
    metastore: std::path::PathBuf,
    index: std::path::PathBuf,
}

impl RestoreDirs {
    fn create(target: &Path) -> Result<Self, BackupError> {
        let dirs = Self {
            blocks: target.join(BLOCKS_DIR),
            events: target.join(EVENTS_DIR),
            metastore: target.join(METASTORE_DIR),
            index: target.join(INDEX_DIR),
        };

        [&dirs.blocks, &dirs.events, &dirs.metastore, &dirs.index]
            .iter()
            .try_for_each(std::fs::create_dir_all)?;

        Ok(dirs)
    }

    fn finalize(
        &self,
        manifest: &BackupManifest,
        file_count: impl Fn(&str) -> u32,
    ) -> Result<RestoreResult, BackupError> {
        [&self.blocks, &self.events, &self.metastore]
            .iter()
            .try_for_each(|d| sync_dir(d))?;

        std::fs::create_dir_all(&self.index).map_err(BackupError::Io)?;

        verify_restored_eventlog(&self.events, manifest)?;
        verify_restored_metastore(&self.metastore)?;

        Ok(RestoreResult {
            blocks_files_restored: file_count(BLOCKS_DIR),
            event_segments_restored: file_count(EVENTS_DIR),
            metastore_files_restored: file_count(METASTORE_DIR),
        })
    }
}

pub fn restore_from_backup(source: &Path, target: &Path) -> Result<RestoreResult, BackupError> {
    reject_nonempty_target(target)?;

    let manifest = read_manifest(source)?;
    validate_manifest_checksums(&manifest, source)?;

    let staging = staging_dir(target)?;

    let result = (|| {
        let dirs = RestoreDirs::create(&staging)?;

        manifest
            .files
            .iter()
            .try_for_each(|entry| copy_validated_entry(entry, source, &staging))?;

        dirs.finalize(&manifest, |prefix| {
            u32::try_from(
                manifest
                    .files
                    .iter()
                    .filter(|e| e.path.starts_with(prefix))
                    .count(),
            )
            .unwrap_or(u32::MAX)
        })
    })();

    promote_or_cleanup(staging, target, result)
}

fn validate_incremental_chain(
    base: &BackupManifest,
    incr: &BackupManifest,
) -> Result<(), BackupError> {
    match incr.kind {
        BackupKind::Incremental => {}
        BackupKind::Full => {
            return Err(BackupError::ChainMismatch(
                "incremental manifest has kind=Full".into(),
            ));
        }
    }

    if incr.created_at_ms < base.created_at_ms {
        return Err(BackupError::ChainMismatch(
            "incremental backup predates its base".into(),
        ));
    }

    match (&incr.base_blockstore, &incr.base_eventlog) {
        (Some(recorded_bs), Some(recorded_el)) => {
            if *recorded_bs != base.blockstore {
                return Err(BackupError::ChainMismatch(
                    "incremental blockstore base does not match provided base manifest".into(),
                ));
            }
            if *recorded_el != base.eventlog {
                return Err(BackupError::ChainMismatch(
                    "incremental eventlog base does not match provided base manifest".into(),
                ));
            }
        }
        _ => {
            return Err(BackupError::ChainMismatch(
                "incremental manifest missing base_blockstore or base_eventlog".into(),
            ));
        }
    }

    if incr.blockstore.epoch < base.blockstore.epoch {
        return Err(BackupError::ChainMismatch(
            "incremental blockstore epoch regressed from base".into(),
        ));
    }

    if incr.eventlog.max_seq < base.eventlog.max_seq {
        return Err(BackupError::ChainMismatch(
            "incremental eventlog max_seq regressed from base".into(),
        ));
    }

    Ok(())
}

pub fn restore_from_incremental(
    base_dir: &Path,
    incremental_dir: &Path,
    target: &Path,
) -> Result<RestoreResult, BackupError> {
    reject_nonempty_target(target)?;

    let base_manifest = read_manifest(base_dir)?;
    let incr_manifest = read_manifest(incremental_dir)?;

    validate_incremental_chain(&base_manifest, &incr_manifest)?;

    validate_manifest_checksums(&base_manifest, base_dir)?;
    validate_manifest_checksums(&incr_manifest, incremental_dir)?;

    let staging = staging_dir(target)?;

    let result = (|| {
        let dirs = RestoreDirs::create(&staging)?;

        let incr_paths: HashSet<&str> = incr_manifest
            .files
            .iter()
            .map(|e| e.path.as_str())
            .collect();

        base_manifest
            .files
            .iter()
            .filter(|entry| !incr_paths.contains(entry.path.as_str()))
            .try_for_each(|entry| copy_validated_entry(entry, base_dir, &staging))?;

        incr_manifest
            .files
            .iter()
            .try_for_each(|entry| copy_validated_entry(entry, incremental_dir, &staging))?;

        dirs.finalize(&incr_manifest, |prefix| {
            let from_base = base_manifest
                .files
                .iter()
                .filter(|e| e.path.starts_with(prefix) && !incr_paths.contains(e.path.as_str()))
                .count();
            let from_incr = incr_manifest
                .files
                .iter()
                .filter(|e| e.path.starts_with(prefix))
                .count();
            u32::try_from(from_base.saturating_add(from_incr)).unwrap_or(u32::MAX)
        })
    })();

    promote_or_cleanup(staging, target, result)
}

pub fn recover_to_sequence(
    backup: &Path,
    eventlog_archive: &Path,
    target_seq: EventSequence,
    target: &Path,
) -> Result<PitrResult, BackupError> {
    let manifest = read_manifest(backup)?;
    let backup_max_seq = manifest.eventlog.max_seq;

    if target_seq < backup_max_seq {
        return Err(BackupError::RestoreVerification(format!(
            "target_seq ({target_seq}) precedes backup max_seq ({backup_max_seq})",
        )));
    }

    let restore = restore_from_backup(backup, target)?;

    if target_seq == backup_max_seq {
        return Ok(PitrResult {
            restore,
            events_replayed: 0,
            target_seq,
        });
    }

    let target_events = target.join(EVENTS_DIR);
    merge_archived_segments(eventlog_archive, &target_events, &manifest)?;

    let metastore = Metastore::open(&target.join(METASTORE_DIR), MetastoreConfig::default())?;

    let eventlog = EventLog::open(
        EventLogConfig {
            segments_dir: target_events,
            ..EventLogConfig::default()
        },
        RealIO::new(),
    )?;

    let eventlog_max = eventlog.max_seq();
    if target_seq > eventlog_max {
        let _ = eventlog.shutdown();
        return Err(BackupError::RestoreVerification(format!(
            "target_seq ({target_seq}) exceeds available eventlog max_seq ({eventlog_max})",
        )));
    }

    let events_replayed =
        replay_mutations_bounded(&eventlog, &metastore, backup_max_seq, target_seq)?;

    metastore.persist()?;
    let _ = eventlog.shutdown();

    Ok(PitrResult {
        restore,
        events_replayed,
        target_seq,
    })
}

const PITR_BATCH_SIZE: usize = 4096;

fn merge_archived_segments(
    archive: &Path,
    target_events: &Path,
    manifest: &BackupManifest,
) -> Result<(), BackupError> {
    let backup_active_id = manifest.eventlog.active_segment_id;

    std::fs::read_dir(archive)?
        .filter(|entry| {
            entry.as_ref().map_or(true, |e| {
                e.path()
                    .extension()
                    .and_then(|ext| ext.to_str())
                    .is_some_and(|ext| ext == "tqe")
            })
        })
        .try_for_each(|entry| {
            let entry = entry?;
            let src = entry.path();
            let file_name = entry.file_name();
            let name = file_name.to_string_lossy();
            let seg_id = name
                .strip_suffix(".tqe")
                .and_then(|s| s.parse::<u32>().ok())
                .map(SegmentId::new);

            match seg_id {
                Some(id) if id > backup_active_id => {
                    let dest = target_events.join(&*file_name);
                    copy_file_synced(&src, &dest, None)?;
                    Ok::<(), io::Error>(())
                }
                Some(id) if id == backup_active_id => {
                    let archive_size = entry.metadata()?.len();
                    let backup_size = manifest.eventlog.active_segment_position.raw();
                    if archive_size > backup_size {
                        let dest = target_events.join(&*file_name);
                        copy_file_synced(&src, &dest, None)?;
                    }
                    Ok(())
                }
                _ => Ok(()),
            }
        })?;

    sync_dir(target_events)?;
    Ok(())
}

fn ewm_to_event_sequence(ewm: &EventWithMutations) -> Result<EventSequence, BackupError> {
    EventSequence::try_from(ewm.event.seq).map_err(|reason| {
        BackupError::RestoreVerification(format!(
            "event sequence {} not convertible: {reason}",
            ewm.event.seq
        ))
    })
}

fn replay_mutations_bounded(
    eventlog: &EventLog<RealIO>,
    metastore: &Metastore,
    from_seq: EventSequence,
    target_seq: EventSequence,
) -> Result<u64, BackupError> {
    let repo_data = metastore.partition(Partition::RepoData);
    let indexes = metastore.partition(Partition::Indexes);
    let db = metastore.database();
    let cursor_key = metastore_cursor_key();

    let mut cursor = from_seq;
    let mut total = 0u64;

    loop {
        let page = eventlog.get_events_with_mutations_since(cursor, PITR_BATCH_SIZE)?;
        let reached_end = page.len() < PITR_BATCH_SIZE;

        let cutoff = page
            .iter()
            .position(|ewm| ewm_to_event_sequence(ewm).is_ok_and(|es| es > target_seq))
            .unwrap_or(page.len());

        let (new_cursor, new_total) =
            page[..cutoff]
                .iter()
                .try_fold((cursor, total), |(_, count), ewm| {
                    let event_es = ewm_to_event_sequence(ewm)?;
                    replay_single_event(db, repo_data, indexes, &cursor_key, ewm, event_es)?;
                    Ok::<_, BackupError>((event_es, count.saturating_add(1)))
                })?;

        cursor = new_cursor;
        total = new_total;

        if cutoff < page.len() || reached_end {
            return Ok(total);
        }
    }
}

fn replay_single_event(
    db: &fjall::Database,
    repo_data: &fjall::Keyspace,
    indexes: &fjall::Keyspace,
    cursor_key: &[u8],
    ewm: &EventWithMutations,
    event_es: EventSequence,
) -> Result<(), BackupError> {
    let seq_raw = event_es.raw();
    let user_hash = UserHash::from_did(ewm.event.did.as_str());
    let mut batch = db.batch();

    let de_key = did_events_key(user_hash, seq_raw);
    batch.insert(repo_data, de_key.as_slice(), []);

    if let Some(rev) = &ewm.event.rev {
        let rs_key = rev_to_seq_key(user_hash, rev);
        batch.insert(repo_data, rs_key.as_slice(), seq_raw.to_be_bytes());
    }

    if let Some(ms_bytes) = &ewm.mutation_set {
        let ms = CommitMutationSet::deserialize(ms_bytes).ok_or_else(|| {
            BackupError::RestoreVerification(format!("corrupt CommitMutationSet at seq {seq_raw}"))
        })?;

        let meta_key = repo_meta_key(user_hash);
        let current_meta = repo_data
            .get(meta_key.as_slice())
            .map_err(MetastoreError::from)?
            .and_then(|raw| RepoMetaValue::deserialize(&raw))
            .unwrap_or_else(|| RepoMetaValue {
                repo_root_cid: vec![],
                repo_rev: String::new(),
                handle: String::new(),
                status: RepoStatus::Active,
                deactivated_at_ms: None,
                takedown_ref: None,
                did: Some(ewm.event.did.as_str().to_owned()),
            });

        replay_mutation_set(
            &mut batch,
            repo_data,
            indexes,
            user_hash,
            &current_meta,
            &ms,
        )
        .map_err(BackupError::Metastore)?;
    }

    batch.insert(repo_data, cursor_key, seq_raw.to_be_bytes());
    batch.commit().map_err(MetastoreError::from)?;

    Ok(())
}

fn reject_nonempty_target(target: &Path) -> Result<(), BackupError> {
    match std::fs::read_dir(target) {
        Ok(mut entries) => match entries.next() {
            Some(_) => Err(BackupError::TargetNotEmpty(target.display().to_string())),
            None => Ok(()),
        },
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
        Err(e) => Err(BackupError::Io(e)),
    }
}

fn staging_dir(target: &Path) -> Result<std::path::PathBuf, BackupError> {
    let parent = target.parent().ok_or_else(|| {
        BackupError::Io(io::Error::new(
            io::ErrorKind::InvalidInput,
            "target path has no parent directory",
        ))
    })?;
    std::fs::create_dir_all(parent)?;
    let stem = target
        .file_name()
        .map(|n| n.to_string_lossy())
        .unwrap_or_default();
    let staging = parent.join(format!(".{stem}.restore-staging"));
    if staging.exists() {
        std::fs::remove_dir_all(&staging)?;
    }
    std::fs::create_dir_all(&staging)?;
    Ok(staging)
}

fn promote_or_cleanup<T>(
    staging: std::path::PathBuf,
    target: &Path,
    result: Result<T, BackupError>,
) -> Result<T, BackupError> {
    match result {
        Ok(val) => {
            std::fs::rename(&staging, target)?;
            if let Some(parent) = target.parent() {
                sync_dir(parent)?;
            }
            Ok(val)
        }
        Err(e) => {
            let _ = std::fs::remove_dir_all(&staging);
            Err(e)
        }
    }
}

fn validate_manifest_checksums(
    manifest: &BackupManifest,
    backup_dir: &Path,
) -> Result<(), BackupError> {
    manifest.files.iter().try_for_each(|entry| {
        let file_path = backup_dir.join(&entry.path);

        let metadata = std::fs::metadata(&file_path).map_err(|e| match e.kind() {
            io::ErrorKind::NotFound => BackupError::MissingFile(entry.path.clone()),
            _ => BackupError::Io(e),
        })?;

        let actual_size = metadata.len();
        if actual_size != entry.size {
            return Err(BackupError::SizeMismatch {
                path: entry.path.clone(),
                expected: entry.size,
                actual: actual_size,
            });
        }

        let actual_checksum = checksum_file(&file_path)?;
        if actual_checksum != entry.xxh3_checksum {
            return Err(BackupError::ChecksumMismatch {
                path: entry.path.clone(),
                expected: entry.xxh3_checksum,
                actual: actual_checksum,
            });
        }

        Ok(())
    })
}

fn verify_restored_eventlog(
    events_dir: &Path,
    manifest: &BackupManifest,
) -> Result<(), BackupError> {
    use crate::eventlog::{DEFAULT_INDEX_INTERVAL, MAX_EVENT_PAYLOAD, rebuild_from_segment};

    if manifest.eventlog.max_seq == EventSequence::BEFORE_ALL {
        return Ok(());
    }

    let real_io = RealIO::new();
    let active_path = events_dir.join(format!("{}.tqe", manifest.eventlog.active_segment_id));
    let fd = real_io.open(&active_path, crate::OpenOptions::read_only_existing())?;
    let (_, last_seq) =
        match rebuild_from_segment(&real_io, fd, DEFAULT_INDEX_INTERVAL, MAX_EVENT_PAYLOAD) {
            Ok(result) => {
                real_io.close(fd)?;
                result
            }
            Err(e) => {
                let _ = real_io.close(fd);
                return Err(e.into());
            }
        };

    let restored_max = last_seq.unwrap_or(EventSequence::BEFORE_ALL);
    if restored_max != manifest.eventlog.max_seq {
        return Err(BackupError::RestoreVerification(format!(
            "eventlog max_seq mismatch: manifest={}, restored={}",
            manifest.eventlog.max_seq, restored_max,
        )));
    }

    Ok(())
}

fn verify_restored_metastore(metastore_dir: &Path) -> Result<(), BackupError> {
    let _metastore = Metastore::open(metastore_dir, MetastoreConfig::default())?;
    Ok(())
}

pub fn verify_backup(source: &Path) -> Result<VerifyResult, BackupError> {
    let manifest = read_manifest(source)?;

    let file_failures: Vec<FileFailure> = manifest
        .files
        .iter()
        .filter_map(|entry| {
            let file_path = source.join(&entry.path);
            match std::fs::metadata(&file_path) {
                Err(e) => return Some(FileFailure::Unreadable(entry.path.clone(), e)),
                Ok(m) if m.len() != entry.size => {
                    return Some(FileFailure::SizeMismatch(entry.path.clone()));
                }
                _ => {}
            }
            match checksum_file(&file_path) {
                Ok(actual) if actual != entry.xxh3_checksum => {
                    Some(FileFailure::ChecksumMismatch(entry.path.clone()))
                }
                Err(e) => Some(FileFailure::Unreadable(entry.path.clone(), e)),
                _ => None,
            }
        })
        .collect();

    let io = RealIO::new();
    let blocks_dir = source.join(BLOCKS_DIR);
    let (total_blocks, corrupted_blocks) = match blocks_dir.is_dir() {
        true => verify_blockstore_integrity(&io, &blocks_dir)?,
        false => (0, 0),
    };

    let events_dir = source.join(EVENTS_DIR);
    let (total_events, corrupted_events) = match events_dir.is_dir() {
        true => verify_eventlog_integrity(&io, &events_dir)?,
        false => (0, 0),
    };

    Ok(VerifyResult {
        total_blocks,
        total_events,
        corrupted_blocks,
        corrupted_events,
        file_failures,
    })
}

enum RecordHealth {
    Valid,
    Corrupted,
}

fn tally_record_health(records: impl Iterator<Item = RecordHealth>) -> (u64, u64) {
    records.fold((0u64, 0u64), |(total, corrupted), health| match health {
        RecordHealth::Valid => (total.saturating_add(1), corrupted),
        RecordHealth::Corrupted => (total.saturating_add(1), corrupted.saturating_add(1)),
    })
}

fn verify_blockstore_integrity<S: StorageIO>(
    io: &S,
    blocks_dir: &Path,
) -> Result<(u64, u64), BackupError> {
    use crate::blockstore::{DataFileReader, ReadBlockRecord, list_files_by_extension};

    let file_ids = list_files_by_extension(io, blocks_dir, "tqb")?;

    file_ids.iter().try_fold(
        (0u64, 0u64),
        |(total, corrupted), &file_id| -> Result<(u64, u64), BackupError> {
            let path = blocks_dir.join(format!("{file_id}.tqb"));
            let fd = io.open(&path, crate::OpenOptions::read_only_existing())?;
            let reader = match DataFileReader::open(io, fd) {
                Ok(r) => r,
                Err(e) => {
                    let _ = io.close(fd);
                    return Err(e.into());
                }
            };

            let (ft, fc) = tally_record_health(reader.map(|r| match r {
                Ok(ReadBlockRecord::Valid { .. }) => RecordHealth::Valid,
                _ => RecordHealth::Corrupted,
            }));

            io.close(fd)?;
            Ok((total.saturating_add(ft), corrupted.saturating_add(fc)))
        },
    )
}

fn verify_eventlog_integrity<S: StorageIO>(
    io: &S,
    events_dir: &Path,
) -> Result<(u64, u64), BackupError> {
    use crate::eventlog::{MAX_EVENT_PAYLOAD, ReadEventRecord, SegmentReader};

    let entries = io.list_dir(events_dir)?;
    let mut segment_paths: Vec<std::path::PathBuf> = entries
        .into_iter()
        .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("tqe"))
        .collect();
    segment_paths.sort();

    segment_paths.iter().try_fold(
        (0u64, 0u64),
        |(total, corrupted), path| -> Result<(u64, u64), BackupError> {
            let fd = io.open(path, crate::OpenOptions::read_only_existing())?;
            let reader = match SegmentReader::open(io, fd, MAX_EVENT_PAYLOAD) {
                Ok(r) => r,
                Err(e) => {
                    let _ = io.close(fd);
                    return Err(e.into());
                }
            };

            let (st, sc) = tally_record_health(reader.map(|r| match r {
                Ok(ReadEventRecord::Valid { .. }) => RecordHealth::Valid,
                _ => RecordHealth::Corrupted,
            }));

            io.close(fd)?;
            Ok((total.saturating_add(st), corrupted.saturating_add(sc)))
        },
    )
}

fn sync_dir(path: &Path) -> io::Result<()> {
    let dir = std::fs::File::open(path)?;
    dir.sync_all()
}

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

    fn make_full_manifest(files: Vec<BackupFileEntry>) -> BackupManifest {
        BackupManifest {
            version: BACKUP_FORMAT_VERSION,
            created_at_ms: 1_700_000_000_000,
            blockstore: BlockstoreManifest {
                write_cursor_file_id: DataFileId::new(5),
                write_cursor_offset: BlockOffset::new(102400),
                epoch: CommitEpoch::new(42),
                shard_cursors: vec![ShardCursorEntry {
                    file_id: DataFileId::new(5),
                    offset: BlockOffset::new(102400),
                }],
            },
            eventlog: EventLogManifest {
                max_seq: EventSequence::new(1000),
                active_segment_id: SegmentId::new(3),
                active_segment_position: SegmentOffset::new(32768),
            },
            metastore_seqno: MetastoreSeqno::new(5000),
            files,
            kind: BackupKind::Full,
            base_blockstore: None,
            base_eventlog: None,
        }
    }

    #[test]
    fn manifest_round_trip() {
        let manifest = make_full_manifest(vec![
            BackupFileEntry {
                path: "blocks/000000.tqb".into(),
                size: 256000,
                xxh3_checksum: 0xDEAD_BEEF_CAFE_1234,
            },
            BackupFileEntry {
                path: "events/00000001.tqe".into(),
                size: 64000,
                xxh3_checksum: 0x1234_5678_9ABC_DEF0,
            },
        ]);

        let json = serde_json::to_string_pretty(&manifest).unwrap();
        let decoded: BackupManifest = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.version, manifest.version);
        assert_eq!(decoded.blockstore.write_cursor_file_id, DataFileId::new(5));
        assert_eq!(decoded.blockstore.epoch, CommitEpoch::new(42));
        assert_eq!(decoded.eventlog.max_seq, EventSequence::new(1000));
        assert_eq!(decoded.metastore_seqno, MetastoreSeqno::new(5000));
        assert_eq!(decoded.files.len(), 2);
        assert_eq!(decoded.files[0].xxh3_checksum, 0xDEAD_BEEF_CAFE_1234);
        assert_eq!(decoded.kind, BackupKind::Full);
        assert!(decoded.base_blockstore.is_none());
    }

    #[test]
    fn checksum_deterministic() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("test.bin");
        std::fs::write(&path, b"hello world checksum test").unwrap();

        let c1 = checksum_file(&path).unwrap();
        let c2 = checksum_file(&path).unwrap();
        assert_eq!(c1, c2);
        assert_ne!(c1, 0);
    }

    #[test]
    fn copy_file_synced_full() {
        let dir = tempfile::TempDir::new().unwrap();
        let src = dir.path().join("src.bin");
        let dest = dir.path().join("dest.bin");
        let data = vec![0xABu8; 1024];
        std::fs::write(&src, &data).unwrap();

        let copied = copy_file_synced(&src, &dest, None).unwrap();
        assert_eq!(copied, 1024);
        assert_eq!(std::fs::read(&dest).unwrap(), data);
    }

    #[test]
    fn copy_file_synced_partial() {
        let dir = tempfile::TempDir::new().unwrap();
        let src = dir.path().join("src.bin");
        let dest = dir.path().join("dest.bin");
        let data = vec![0xCDu8; 1024];
        std::fs::write(&src, &data).unwrap();

        let copied = copy_file_synced(&src, &dest, Some(512)).unwrap();
        assert_eq!(copied, 512);

        let result = std::fs::read(&dest).unwrap();
        assert_eq!(result.len(), 512);
        assert_eq!(result, &data[..512]);
    }

    #[test]
    fn copy_dir_recursive_skips_lock() {
        let dir = tempfile::TempDir::new().unwrap();
        let src = dir.path().join("src");
        let dest = dir.path().join("dest");
        std::fs::create_dir_all(src.join("sub")).unwrap();
        std::fs::write(src.join("data.sst"), b"sst data").unwrap();
        std::fs::write(src.join("sub/nested.sst"), b"nested").unwrap();
        std::fs::write(src.join(".lock"), b"locked").unwrap();

        let mut files = Vec::new();
        copy_metastore_files(&src, &dest, &mut files).unwrap();

        assert!(dest.join("data.sst").exists());
        assert!(dest.join("sub/nested.sst").exists());
        assert!(!dest.join(".lock").exists());
        assert_eq!(files.len(), 2);
        assert!(files.iter().all(|f| f.path.starts_with(METASTORE_DIR)));
    }

    #[test]
    fn manifest_write_and_read() {
        let dir = tempfile::TempDir::new().unwrap();
        let manifest = make_full_manifest(Vec::new());

        write_manifest(&manifest, dir.path()).unwrap();
        let loaded = read_manifest(dir.path()).unwrap();
        assert_eq!(loaded.version, BACKUP_FORMAT_VERSION);
        assert_eq!(loaded.kind, BackupKind::Full);
    }

    #[test]
    fn read_manifest_rejects_unknown_version() {
        let dir = tempfile::TempDir::new().unwrap();
        let mut manifest = make_full_manifest(Vec::new());
        manifest.version = 999;

        let json = serde_json::to_string_pretty(&manifest).unwrap();
        std::fs::write(dir.path().join(MANIFEST_FILENAME), json.as_bytes()).unwrap();

        let err = read_manifest(dir.path()).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn verify_copy_size_mismatch() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("dummy.bin");
        std::fs::write(&path, b"").unwrap();

        let err = verify_copy_size(100, 200, &path).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
    }

    #[test]
    fn legacy_manifest_without_kind_deserializes_as_full() {
        let json = r#"{
            "version": 1,
            "created_at_ms": 42,
            "blockstore": {
                "write_cursor_file_id": 0,
                "write_cursor_offset": 0,
                "epoch": 0
            },
            "eventlog": {
                "max_seq": 0,
                "active_segment_id": 0,
                "active_segment_position": 0
            },
            "metastore_seqno": 0,
            "files": []
        }"#;

        let decoded: BackupManifest = serde_json::from_str(json).unwrap();
        assert_eq!(decoded.kind, BackupKind::Full);
        assert!(decoded.base_blockstore.is_none());
        assert!(decoded.base_eventlog.is_none());
    }

    #[test]
    fn incremental_manifest_round_trip() {
        let base_bs = BlockstoreManifest {
            write_cursor_file_id: DataFileId::new(3),
            write_cursor_offset: BlockOffset::new(50000),
            epoch: CommitEpoch::new(10),
            shard_cursors: vec![ShardCursorEntry {
                file_id: DataFileId::new(3),
                offset: BlockOffset::new(50000),
            }],
        };
        let base_el = EventLogManifest {
            max_seq: EventSequence::new(500),
            active_segment_id: SegmentId::new(2),
            active_segment_position: SegmentOffset::new(16384),
        };

        let manifest = BackupManifest {
            version: BACKUP_FORMAT_VERSION,
            created_at_ms: 1_700_000_000_000,
            blockstore: BlockstoreManifest {
                write_cursor_file_id: DataFileId::new(7),
                write_cursor_offset: BlockOffset::new(200000),
                epoch: CommitEpoch::new(50),
                shard_cursors: vec![ShardCursorEntry {
                    file_id: DataFileId::new(7),
                    offset: BlockOffset::new(200000),
                }],
            },
            eventlog: EventLogManifest {
                max_seq: EventSequence::new(2000),
                active_segment_id: SegmentId::new(5),
                active_segment_position: SegmentOffset::new(65536),
            },
            metastore_seqno: MetastoreSeqno::new(9000),
            files: vec![BackupFileEntry {
                path: "blocks/000007.tqb".into(),
                size: 200000,
                xxh3_checksum: 0xAAAA_BBBB_CCCC_DDDD,
            }],
            kind: BackupKind::Incremental,
            base_blockstore: Some(base_bs.clone()),
            base_eventlog: Some(base_el.clone()),
        };

        let json = serde_json::to_string_pretty(&manifest).unwrap();
        let decoded: BackupManifest = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.kind, BackupKind::Incremental);
        let decoded_base_bs = decoded.base_blockstore.unwrap();
        assert_eq!(decoded_base_bs.write_cursor_file_id, DataFileId::new(3));
        assert_eq!(decoded_base_bs.write_cursor_offset, BlockOffset::new(50000));
        let decoded_base_el = decoded.base_eventlog.unwrap();
        assert_eq!(decoded_base_el.active_segment_id, SegmentId::new(2));
    }

    #[test]
    fn incremental_blockstore_filters_old_files() {
        let base = BlockstoreManifest {
            write_cursor_file_id: DataFileId::new(3),
            write_cursor_offset: BlockOffset::new(50000),
            epoch: CommitEpoch::new(10),
            shard_cursors: vec![ShardCursorEntry {
                file_id: DataFileId::new(3),
                offset: BlockOffset::new(50000),
            }],
        };

        let snapshot = BlockstoreSnapshot {
            shard_cursors: vec![WriteCursor {
                file_id: DataFileId::new(5),
                offset: BlockOffset::new(1024),
            }],
            epoch: CommitEpoch::new(15),
            data_files: vec![
                DataFileId::new(0),
                DataFileId::new(1),
                DataFileId::new(2),
                DataFileId::new(3),
                DataFileId::new(4),
                DataFileId::new(5),
            ],
        };

        let dir = tempfile::TempDir::new().unwrap();
        let data_dir = dir.path().join("data");
        let dest_dir = dir.path().join("dest");
        std::fs::create_dir_all(&data_dir).unwrap();

        (0u32..=5).for_each(|id| {
            let path = data_dir.join(format!("{}.tqb", DataFileId::new(id)));
            std::fs::write(&path, vec![0xABu8; 2048]).unwrap();
        });

        let mut files = Vec::new();
        copy_blockstore_files(
            &snapshot,
            base.write_cursor_file_id,
            &data_dir,
            &dest_dir,
            &mut files,
        )
        .unwrap();

        let copied_ids: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();

        assert_eq!(copied_ids.len(), 3);
        assert!(copied_ids.contains(&"blocks/000003.tqb"));
        assert!(copied_ids.contains(&"blocks/000004.tqb"));
        assert!(copied_ids.contains(&"blocks/000005.tqb"));

        assert!(!dest_dir.join("000000.tqb").exists());
        assert!(!dest_dir.join("000001.tqb").exists());
        assert!(!dest_dir.join("000002.tqb").exists());
    }

    #[test]
    fn incremental_eventlog_filters_old_segments() {
        let base = EventLogManifest {
            max_seq: EventSequence::new(500),
            active_segment_id: SegmentId::new(2),
            active_segment_position: SegmentOffset::new(8192),
        };

        let snapshot = EventLogSnapshotState {
            max_seq: EventSequence::new(1500),
            active_segment_id: SegmentId::new(4),
            active_segment_position: SegmentOffset::new(4096),
            sealed_segments: vec![
                SegmentId::new(0),
                SegmentId::new(1),
                SegmentId::new(2),
                SegmentId::new(3),
            ],
        };

        let dir = tempfile::TempDir::new().unwrap();
        let seg_dir = dir.path().join("segments");
        let dest_dir = dir.path().join("dest");
        std::fs::create_dir_all(&seg_dir).unwrap();

        (0u32..=4).for_each(|id| {
            let path = seg_dir.join(format!("{}.tqe", SegmentId::new(id)));
            std::fs::write(&path, vec![0xCDu8; 4096]).unwrap();
        });

        let mut files = Vec::new();
        copy_eventlog_files(
            &snapshot,
            base.active_segment_id,
            &seg_dir,
            &dest_dir,
            &mut files,
        )
        .unwrap();

        let copied_ids: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();

        assert_eq!(copied_ids.len(), 3);
        assert!(copied_ids.contains(&"events/00000002.tqe"));
        assert!(copied_ids.contains(&"events/00000003.tqe"));
        assert!(copied_ids.contains(&"events/00000004.tqe"));

        assert!(!dest_dir.join("00000000.tqe").exists());
        assert!(!dest_dir.join("00000001.tqe").exists());
    }

    #[test]
    fn incremental_no_change_produces_minimal_delta() {
        let base = BlockstoreManifest {
            write_cursor_file_id: DataFileId::new(2),
            write_cursor_offset: BlockOffset::new(4096),
            epoch: CommitEpoch::new(5),
            shard_cursors: vec![ShardCursorEntry {
                file_id: DataFileId::new(2),
                offset: BlockOffset::new(4096),
            }],
        };

        let snapshot = BlockstoreSnapshot {
            shard_cursors: vec![WriteCursor {
                file_id: DataFileId::new(2),
                offset: BlockOffset::new(4096),
            }],
            epoch: CommitEpoch::new(5),
            data_files: vec![DataFileId::new(0), DataFileId::new(1), DataFileId::new(2)],
        };

        let dir = tempfile::TempDir::new().unwrap();
        let data_dir = dir.path().join("data");
        let dest_dir = dir.path().join("dest");
        std::fs::create_dir_all(&data_dir).unwrap();

        (0u32..=2).for_each(|id| {
            let path = data_dir.join(format!("{}.tqb", DataFileId::new(id)));
            std::fs::write(&path, vec![0u8; 4096]).unwrap();
        });

        let mut files = Vec::new();
        copy_blockstore_files(
            &snapshot,
            base.write_cursor_file_id,
            &data_dir,
            &dest_dir,
            &mut files,
        )
        .unwrap();

        assert_eq!(files.len(), 1);
        assert!(files[0].path.contains("000002"));
    }

    #[test]
    fn chain_validation_rejects_kind_full() {
        let base = make_full_manifest(Vec::new());
        let incr = make_full_manifest(Vec::new());

        let err = validate_incremental_chain(&base, &incr).unwrap_err();
        assert!(matches!(err, BackupError::ChainMismatch(_)));
    }

    #[test]
    fn chain_validation_rejects_mismatched_base() {
        let base = make_full_manifest(Vec::new());
        let mut incr = make_full_manifest(Vec::new());
        incr.kind = BackupKind::Incremental;
        incr.base_blockstore = Some(BlockstoreManifest {
            write_cursor_file_id: DataFileId::new(99),
            write_cursor_offset: BlockOffset::new(0),
            epoch: CommitEpoch::new(0),
            shard_cursors: Vec::new(),
        });
        incr.base_eventlog = Some(base.eventlog.clone());

        let err = validate_incremental_chain(&base, &incr).unwrap_err();
        assert!(matches!(err, BackupError::ChainMismatch(_)));
    }

    #[test]
    fn chain_validation_accepts_matching_base() {
        let base = make_full_manifest(Vec::new());
        let mut incr = make_full_manifest(Vec::new());
        incr.kind = BackupKind::Incremental;
        incr.base_blockstore = Some(base.blockstore.clone());
        incr.base_eventlog = Some(base.eventlog.clone());

        validate_incremental_chain(&base, &incr).unwrap();
    }

    #[test]
    fn chain_validation_rejects_timestamp_regression() {
        let base = make_full_manifest(Vec::new());
        let mut incr = make_full_manifest(Vec::new());
        incr.kind = BackupKind::Incremental;
        incr.base_blockstore = Some(base.blockstore.clone());
        incr.base_eventlog = Some(base.eventlog.clone());
        incr.created_at_ms = base.created_at_ms - 1;

        let err = validate_incremental_chain(&base, &incr).unwrap_err();
        assert!(matches!(err, BackupError::ChainMismatch(_)));
    }

    #[test]
    fn chain_validation_rejects_epoch_regression() {
        let base = make_full_manifest(Vec::new());
        let mut incr = make_full_manifest(Vec::new());
        incr.kind = BackupKind::Incremental;
        incr.base_blockstore = Some(base.blockstore.clone());
        incr.base_eventlog = Some(base.eventlog.clone());
        incr.blockstore.epoch = CommitEpoch::new(base.blockstore.epoch.raw() - 1);

        let err = validate_incremental_chain(&base, &incr).unwrap_err();
        assert!(matches!(err, BackupError::ChainMismatch(_)));
    }

    #[test]
    fn chain_validation_rejects_eventlog_regression() {
        let base = make_full_manifest(Vec::new());
        let mut incr = make_full_manifest(Vec::new());
        incr.kind = BackupKind::Incremental;
        incr.base_blockstore = Some(base.blockstore.clone());
        incr.base_eventlog = Some(base.eventlog.clone());
        incr.eventlog.max_seq = EventSequence::new(base.eventlog.max_seq.raw() - 1);

        let err = validate_incremental_chain(&base, &incr).unwrap_err();
        assert!(matches!(err, BackupError::ChainMismatch(_)));
    }
}
