use std::{
    fs::{self, File, OpenOptions},
    io::Write,
    path::{Path, PathBuf},
};

use super::error::WalError;
use crate::replication::ShardId;

/// Format a segment file name from the shard ID and first sequence number.
///
/// For `ShardId::SINGLE` (shard 0), produces the v1 format:
///   `wal-00000000000000000001.seg`
///
/// For any other shard, produces the v2 format:
///   `wal-s00003-00000000000000000001.seg`
///
/// Zero-padded fields ensure lexicographic ordering matches numeric ordering.
#[must_use]
pub fn segment_filename(shard_id: ShardId, first_seq: u64) -> String {
    if shard_id == ShardId::SINGLE {
        format!("wal-{first_seq:020}.seg")
    } else {
        format!("wal-s{:05}-{:020}.seg", shard_id.0, first_seq)
    }
}

/// Parse the shard ID and first sequence number from a WAL segment filename.
///
/// Accepts both formats:
///   - `wal-{first_seq:020}.seg` (single-shard, v1) -> `(ShardId::SINGLE, first_seq)`
///   - `wal-s{shard_id:05}-{first_seq:020}.seg` (multi-shard, v2) -> `(ShardId(n), first_seq)`
///
/// Returns `None` if the filename does not match either format.
#[must_use]
pub fn parse_segment_filename(filename: &str) -> Option<(ShardId, u64)> {
    let name = filename.strip_suffix(".seg")?;

    // Multi-shard format: wal-s{shard_id}-{first_seq}
    if let Some(rest) = name.strip_prefix("wal-s") {
        let dash = rest.find('-')?;
        let shard_id: u16 = rest[..dash].parse().ok()?;
        let first_seq: u64 = rest[dash + 1..].parse().ok()?;
        return Some((ShardId(shard_id), first_seq));
    }

    // Single-shard format: wal-{first_seq}
    if let Some(seq_str) = name.strip_prefix("wal-") {
        let first_seq: u64 = seq_str.parse().ok()?;
        return Some((ShardId::SINGLE, first_seq));
    }

    None
}

/// Parse the first sequence number from a segment file name.
///
/// Backward-compatible wrapper around [`parse_segment_filename`] that discards
/// the shard ID. Used by [`list_segments`] and legacy callers.
///
/// Returns `None` if the file name does not match the expected pattern.
#[must_use]
pub fn parse_segment_seq(filename: &str) -> Option<u64> {
    parse_segment_filename(filename).map(|(_, seq)| seq)
}

/// List all WAL segment files in the directory, sorted by first sequence number.
///
/// # Errors
///
/// Returns `WalError::Io` on filesystem failure.
pub fn list_segments(dir: &Path) -> Result<Vec<(u64, PathBuf)>, WalError> {
    let mut segments = Vec::new();

    let entries = match fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(segments),
        Err(e) => return Err(WalError::Io(e)),
    };

    for entry in entries {
        let entry = entry?;
        let filename = entry.file_name();
        let Some(name) = filename.to_str() else {
            continue;
        };
        if let Some(seq) = parse_segment_seq(name) {
            segments.push((seq, entry.path()));
        }
    }

    segments.sort_by_key(|(seq, _)| *seq);
    Ok(segments)
}

/// List WAL segment files for a specific shard, sorted by first sequence number.
///
/// When `shard_id` is `ShardId::SINGLE`, includes all single-shard format segments.
/// When `shard_id > 0`, includes only segments with that shard prefix.
///
/// # Errors
///
/// Returns `WalError::Io` on filesystem failure.
pub fn list_segments_for_shard(
    dir: &Path,
    shard_id: ShardId,
) -> Result<Vec<(u64, PathBuf)>, WalError> {
    let mut segments = Vec::new();

    let entries = match fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(segments),
        Err(e) => return Err(WalError::Io(e)),
    };

    for entry in entries {
        let entry = entry?;
        let file_name = entry.file_name();
        let Some(name) = file_name.to_str() else {
            continue;
        };
        if let Some((seg_shard, seq)) = parse_segment_filename(name)
            && seg_shard == shard_id
        {
            segments.push((seq, entry.path()));
        }
    }

    segments.sort_by_key(|(seq, _)| *seq);
    Ok(segments)
}

/// Manages the current writable WAL segment file.
///
/// Handles creation of new segment files, tracking file size for rotation,
/// and data sync. Rotation is triggered externally by the writer when
/// the segment exceeds `max_size`.
pub struct SegmentWriter {
    dir: PathBuf,
    file: File,
    current_size: u64,
    max_size: u64,
    first_seq: u64,
    /// The shard this segment belongs to. Controls filename format:
    /// `ShardId::SINGLE` → v1 format, any other shard → v2 format.
    shard_id: ShardId,
}

impl SegmentWriter {
    /// Open or create a segment file for writing.
    ///
    /// `shard_id` controls the filename format:
    /// - `ShardId::SINGLE` (0) → v1 single-node format `wal-{seq:020}.seg`
    /// - Any other shard → v2 multi-shard format `wal-s{shard:05}-{seq:020}.seg`
    ///
    /// If `first_seq` identifies an existing segment, it is opened for append.
    /// Otherwise, a new file is created.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` on filesystem failure.
    pub fn open(
        dir: &Path,
        shard_id: ShardId,
        first_seq: u64,
        max_size: u64,
    ) -> Result<Self, WalError> {
        let filename = segment_filename(shard_id, first_seq);
        let path = dir.join(&filename);
        let is_new = !path.exists();
        let file = OpenOptions::new().create(true).append(true).open(&path)?;

        // Fsync the parent directory so the new directory entry is durable.
        // Without this, a crash after file creation but before the directory
        // metadata is flushed could lose the segment file entirely. On macOS a
        // plain directory fsync does not flush the device cache, so this routes
        // through the F_FULLFSYNC-backed helper.
        if is_new {
            super::sync_dir_durable(dir)?;
        }

        let metadata = file.metadata()?;
        let current_size = metadata.len();

        Ok(Self {
            dir: dir.to_path_buf(),
            file,
            current_size,
            max_size,
            first_seq,
            shard_id,
        })
    }

    /// Write a raw batch of bytes to the current segment.
    ///
    /// Returns the file offset where the batch was written.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` on write failure.
    pub fn write_batch_bytes(&mut self, bytes: &[u8]) -> Result<u64, WalError> {
        let offset = self.current_size;
        self.file.write_all(bytes)?;
        self.current_size += bytes.len() as u64;
        Ok(offset)
    }

    /// Sync this segment's written **data** to stable storage.
    ///
    /// Routes through [`crate::wal::sync_file_durable`], which flushes the file's
    /// data to the device, defeating the drive write cache. On Linux that is
    /// `fdatasync`; on **macOS** a plain `fsync(2)` does NOT flush the device's
    /// volatile write cache (Apple documents this), so the helper issues
    /// `fcntl(fd, F_FULLFSYNC)` instead — without it a power-loss could lose a
    /// batch the WAL had already "fsynced" and acknowledged. For an append to an
    /// **existing** segment — the steady state — this makes the freshly appended
    /// batch fully durable on every supported platform: there is no directory
    /// change, so a single `sync()` suffices.
    ///
    /// It does NOT make a newly created segment's **directory entry** durable.
    /// A data sync says nothing about the parent directory, so a crash right
    /// after a `create + sync()` could lose the file's name even though its
    /// bytes reached the platter. Directory-entry durability for new and rotated
    /// segments is handled separately by the explicit parent-directory sync in
    /// [`SegmentWriter::open`] (on first creation) and [`SegmentWriter::rotate`]
    /// (on rotation); segment *deletions* are made durable by the
    /// parent-directory fsync in [`delete_segments_before`] and
    /// [`crate::wal::compaction`]. All of those route through
    /// [`crate::wal::sync_dir_durable`], so the directory half of the durability
    /// story is also F_FULLFSYNC-backed on macOS. Together those sites complete
    /// the story this method deliberately covers only the data half of — do not
    /// drop them on the assumption that `sync()` already fsyncs the directory.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` on sync failure.
    pub fn sync(&self) -> Result<(), WalError> {
        super::sync_file_durable(&self.file)
    }

    /// Whether the segment has reached its size threshold and should be rotated.
    #[must_use]
    pub const fn needs_rotation(&self) -> bool {
        self.current_size >= self.max_size
    }

    /// The first sequence number in this segment.
    #[must_use]
    pub const fn first_seq(&self) -> u64 {
        self.first_seq
    }

    /// The current file size in bytes.
    #[must_use]
    pub const fn current_size(&self) -> u64 {
        self.current_size
    }

    /// The shard ID used for segment filename formatting.
    #[must_use]
    pub const fn shard_id(&self) -> ShardId {
        self.shard_id
    }

    /// Create a new segment file and return a writer for it.
    ///
    /// Finalizes the current segment (syncs it) and opens a new one using the
    /// same `shard_id` that was set at construction time.
    ///
    /// # Errors
    ///
    /// Returns `WalError::Io` on filesystem failure.
    pub fn rotate(&mut self, new_first_seq: u64) -> Result<(), WalError> {
        // Sync current segment before rotation
        self.sync()?;

        let filename = segment_filename(self.shard_id, new_first_seq);
        let path = self.dir.join(&filename);
        let file = OpenOptions::new().create(true).append(true).open(&path)?;

        // Fsync the parent directory so the new segment's directory entry
        // is durable. Without this, a crash after file creation but before
        // the directory metadata is flushed could lose the new segment. On
        // macOS this routes through the F_FULLFSYNC-backed helper.
        super::sync_dir_durable(&self.dir)?;

        self.file = file;
        self.current_size = 0;
        self.first_seq = new_first_seq;
        Ok(())
    }
}

/// Delete all segment files whose first sequence number is less than `before_seq`.
///
/// After the unlinks, the WAL directory is fsynced via
/// [`crate::wal::sync_dir_durable`] so the deletions are durable — matching
/// [`crate::wal::compaction::compact_segments`], the other durable-delete
/// routine. Without that directory sync a crash after a `remove_file` but
/// before the directory metadata reached the platter could resurrect a deleted
/// segment file. (Resurrection is benign here — the segment is covered by the
/// checkpoint and replays idempotently — but the two deletion paths must agree
/// on durability so a reader cannot mistake the asymmetry for a bug, and so a
/// future change that makes resurrection non-idempotent does not silently
/// regress.)
///
/// # Errors
///
/// Returns `WalError::Io` on filesystem failure. Partial deletion may occur
/// if an error is encountered mid-way; the directory fsync still runs for any
/// unlinks that did complete.
pub fn delete_segments_before(dir: &Path, before_seq: u64) -> Result<usize, WalError> {
    let segments = list_segments(dir)?;
    let mut deleted = 0;
    let mut result = Ok(());
    for (seq, path) in segments {
        if seq < before_seq {
            if let Err(e) = fs::remove_file(&path) {
                result = Err(WalError::Io(e));
                break;
            }
            deleted += 1;
        }
    }
    // Make the unlinks durable, mirroring compaction's directory fsync. Run this
    // even when a mid-way unlink failed, so the deletions that DID complete are
    // durable before we surface the error.
    if deleted > 0 {
        super::sync_dir_durable(dir)?;
    }
    result?;
    Ok(deleted)
}

#[cfg(test)]
#[path = "segment_tests.rs"]
mod tests;

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod durable_delete_tests {
    use super::*;

    /// W32 regression: `delete_segments_before` must make its unlinks durable by
    /// fsyncing the WAL directory (matching `compaction::compact_segments`), so
    /// the two deletion paths agree on durability. We cannot observe the fsync
    /// itself without crash injection, but we pin that the routine still deletes
    /// exactly the older segments and that the directory-fsync code path executes
    /// cleanly (it would error if the helper or its dir-open regressed).
    #[test]
    fn delete_segments_before_deletes_older_and_dir_sync_runs() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        for &seq in &[1u64, 50, 100] {
            let _ = SegmentWriter::open(dir.path(), ShardId::SINGLE, seq, 1024)
                .expect("seed segment open should succeed");
        }

        // floor = 100 -> seqs 1 and 50 are deleted, 100 survives. The directory
        // fsync runs because deleted > 0; a regression in the helper or its
        // dir-open would surface as an Err here.
        let deleted = delete_segments_before(dir.path(), 100).expect("delete should succeed");
        assert_eq!(deleted, 2);

        let remaining = list_segments(dir.path()).expect("list should succeed");
        assert_eq!(remaining.len(), 1);
        assert_eq!(remaining[0].0, 100);
    }

    /// When nothing matches the floor, no directory fsync is issued (deleted ==
    /// 0) and the routine is a clean no-op.
    #[test]
    fn delete_segments_before_noop_skips_dir_sync() {
        let dir = tempfile::tempdir().expect("tempdir creation should succeed");
        let _ = SegmentWriter::open(dir.path(), ShardId::SINGLE, 100, 1024)
            .expect("seed segment open should succeed");

        let deleted = delete_segments_before(dir.path(), 50).expect("delete should succeed");
        assert_eq!(deleted, 0);
        assert_eq!(
            list_segments(dir.path())
                .expect("list should succeed")
                .len(),
            1
        );
    }
}
