use std::fs::{File, OpenOptions};
#[cfg(unix)]
use std::os::unix::fs::FileExt;
use std::path::Path;

use luci_core::{LuciError, Result, SegmentId};

use crate::allocator::BlockAllocator;
use crate::block::{Extent, BLOCK_SIZE, HEADER_SIZE};
use crate::directory::{MetadataSnapshot, SegmentEntry};
use crate::header::{xxh3_checksum, FileHeader, RootPointer};

/// Production storage backend: all data in a single `.luci` file using the
/// block allocator and two-root-pointer atomic commit.
///
/// See [[architecture-storage-format]] for the full design and [[architecture-storage-format#Atomic Commit Protocol]]
/// for the commit sequence.
///
/// # Crash Safety
///
/// At any point, both root pointers reference intact (non-overwritten) metadata
/// blocks. A crash mid-commit is recovered by falling back to the still-valid
/// root. Uncommitted segment writes are orphaned blocks that the allocator does
/// not reference — they are harmlessly reclaimed on the next file open.
#[cfg(unix)]
pub struct SingleFileDirectory {
    file: File,
    header: FileHeader,
    allocator: BlockAllocator,
    /// Last committed metadata — the source of truth for reads.
    committed: MetadataSnapshot,
    /// Segments written but not yet committed.
    pending_segments: Vec<SegmentEntry>,
    /// Segments to remove on next commit.
    pending_removals: Vec<SegmentId>,
    /// Monotonically increasing commit counter.
    generation: u64,
}

#[cfg(unix)]
impl SingleFileDirectory {
    /// Create a new `.luci` file at the given path.
    ///
    /// Fails if a file already exists at `path`.
    pub fn create(path: impl AsRef<Path>) -> Result<Self> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .create_new(true)
            .open(path.as_ref())?;

        let header = FileHeader::new();
        file.write_all_at(&header.to_bytes(), 0)?;
        file.sync_all()?;

        Ok(Self {
            file,
            header,
            allocator: BlockAllocator::new(),
            committed: MetadataSnapshot::empty(),
            pending_segments: Vec::new(),
            pending_removals: Vec::new(),
            generation: 0,
        })
    }

    /// Open an existing `.luci` file, performing crash recovery.
    ///
    /// Validates root pointers, loads metadata from the best available root,
    /// and reconstructs the block allocator. If the active root's checksum is
    /// invalid (torn write), falls back to the inactive root and repairs the
    /// header.
    ///
    /// See [[architecture-storage-format#Crash Recovery]].
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(path.as_ref())?;

        // Read header.
        let mut header_buf = [0u8; HEADER_SIZE as usize];
        file.read_exact_at(&mut header_buf, 0)?;
        let mut header = FileHeader::from_bytes(&header_buf)?;

        // Load metadata with fallback.
        let (committed, used_inactive) = load_metadata(&file, &header)?;

        // If we fell back to the inactive root, repair the header so the
        // valid root is now marked active.
        if used_inactive {
            header.active_root = header.active_root.inactive();
            file.write_all_at(&header.to_bytes(), 0)?;
            file.sync_all()?;
        }

        // Reconstruct allocator from committed state.
        let allocator =
            BlockAllocator::from_state(committed.total_blocks, committed.free_list.clone());

        let generation = committed
            .segments
            .iter()
            .map(|s| s.generation)
            .max()
            .unwrap_or(0);

        Ok(Self {
            file,
            header,
            allocator,
            committed,
            pending_segments: Vec::new(),
            pending_removals: Vec::new(),
            generation,
        })
    }

    /// Write segment data to allocated blocks.
    ///
    /// The segment is not visible to readers until [`commit`](Self::commit)
    /// is called. Data is written to disk immediately so that `commit` only
    /// needs to write metadata and flip the header.
    ///
    /// # Errors
    ///
    /// Returns an error if `data` is empty or the write fails.
    pub fn write_segment(&mut self, segment_id: SegmentId, data: &[u8]) -> Result<()> {
        if data.is_empty() {
            return Err(LuciError::InvalidQuery(
                "cannot write empty segment".into(),
            ));
        }

        let blocks_needed =
            ((data.len() as u64 + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64) as u32;
        let extent = self.allocator.allocate(blocks_needed)?;

        self.file
            .write_all_at(data, extent.start.byte_offset())?;

        self.pending_segments.push(SegmentEntry::new(
            segment_id,
            extent,
            self.generation + 1,
            data.len() as u64,
        ));

        Ok(())
    }

    /// Read committed segment data by segment ID.
    ///
    /// Only committed segments are visible. Returns `LuciError::IndexNotFound`
    /// if the segment does not exist in the committed state.
    pub fn read_segment(&self, segment_id: SegmentId) -> Result<Vec<u8>> {
        let entry = self
            .committed
            .segments
            .iter()
            .find(|e| e.segment_id == segment_id)
            .ok_or_else(|| LuciError::IndexNotFound(format!("segment {segment_id}")))?;

        let mut buf = vec![0u8; entry.data_len as usize];
        self.file
            .read_exact_at(&mut buf, entry.extent.start.byte_offset())?;
        Ok(buf)
    }

    /// Atomically commit all pending segment writes.
    ///
    /// Implements the six-step atomic commit protocol from [[architecture-storage-format]]:
    ///
    /// 1. Segment data already written to blocks (by `write_segment`)
    /// 2. Serialize new metadata to a freshly allocated block (copy-on-write)
    /// 3. Compute checksum, update the inactive root pointer
    /// 4. `fsync` the data file
    /// 5. Write the 4 KB header with the flipped active root flag
    /// 6. `fsync` the header
    ///
    /// The old inactive root's metadata block is freed and included in the new
    /// free list — but never overwritten until after this commit succeeds, so
    /// both roots remain valid at all times for crash recovery.
    pub fn commit(&mut self) -> Result<()> {
        if self.pending_segments.is_empty() && self.pending_removals.is_empty() {
            return Ok(());
        }

        self.generation += 1;

        // Free the old inactive root's metadata block. This is safe because
        // the active root is our fallback — we never touch its block.
        let old_inactive_meta = self.header.inactive_root_pointer().block_id;
        if let Some(block_id) = old_inactive_meta {
            self.allocator.free(Extent::new(block_id, 1));
        }

        // Allocate a fresh block for the new metadata.
        let meta_extent = self.allocator.allocate(1)?;
        let meta_block = meta_extent.start;

        // Build the new snapshot: committed + pending - removals.
        let mut segments = self.committed.segments.clone();
        segments.extend(self.pending_segments.drain(..));

        // Remove merged source segments and free their blocks.
        if !self.pending_removals.is_empty() {
            segments.retain(|entry| {
                if self.pending_removals.contains(&entry.segment_id) {
                    self.allocator.free(entry.extent);
                    false
                } else {
                    true
                }
            });
            self.pending_removals.clear();
        }

        let snapshot = MetadataSnapshot {
            segments,
            total_blocks: self.allocator.total_blocks(),
            free_list: self.allocator.free_list().to_vec(),
            user_metadata: self.committed.user_metadata.clone(),
        };

        assert!(
            snapshot.fits_in_single_block(),
            "metadata overflow chaining not yet implemented"
        );

        // Serialize metadata into a full block (zero-padded for checksum).
        let meta_bytes = snapshot.to_bytes();
        let mut block_buf = vec![0u8; BLOCK_SIZE as usize];
        block_buf[..meta_bytes.len()].copy_from_slice(&meta_bytes);

        // Step 2: write metadata block.
        self.file
            .write_all_at(&block_buf, meta_block.byte_offset())?;

        // Step 3: compute checksum, update header.
        let checksum = xxh3_checksum(&block_buf);
        self.header.commit(meta_block, checksum);

        // Step 4: fsync data.
        self.file.sync_all()?;

        // Step 5: write header.
        self.file.write_all_at(&self.header.to_bytes(), 0)?;

        // Step 6: fsync header.
        self.file.sync_all()?;

        // Update in-memory committed state.
        self.committed = snapshot;

        Ok(())
    }

    /// The currently committed segment entries.
    pub fn segments(&self) -> &[SegmentEntry] {
        &self.committed.segments
    }

    /// The current commit generation.
    pub fn generation(&self) -> u64 {
        self.generation
    }

    /// Set opaque user metadata to be persisted on the next commit.
    pub fn set_user_metadata(&mut self, metadata: Vec<u8>) {
        self.committed.user_metadata = metadata;
    }

    /// Get the persisted user metadata (empty if none).
    pub fn user_metadata(&self) -> &[u8] {
        &self.committed.user_metadata
    }

    /// Total number of free (reusable) blocks tracked by the allocator.
    ///
    /// Exposed for integration testing (free-list reclamation validation).
    pub fn free_block_count(&self) -> u64 {
        self.allocator.free_block_count()
    }

    /// Total number of data blocks the file spans.
    ///
    /// Exposed for integration testing.
    pub fn total_blocks(&self) -> u64 {
        self.allocator.total_blocks()
    }

    /// Mark segments for removal on the next commit.
    pub fn remove_segments(&mut self, segment_ids: &[SegmentId]) {
        self.pending_removals.extend_from_slice(segment_ids);
    }
}

#[cfg(unix)]
impl crate::Storage for SingleFileDirectory {
    fn write_segment(&mut self, segment_id: SegmentId, data: &[u8]) -> Result<()> {
        self.write_segment(segment_id, data)
    }
    fn read_segment(&self, segment_id: SegmentId) -> Result<Vec<u8>> {
        self.read_segment(segment_id)
    }
    fn commit(&mut self) -> Result<()> {
        self.commit()
    }
    fn segments(&self) -> &[SegmentEntry] {
        self.segments()
    }
    fn generation(&self) -> u64 {
        self.generation()
    }
    fn set_user_metadata(&mut self, metadata: Vec<u8>) {
        self.set_user_metadata(metadata)
    }
    fn user_metadata(&self) -> &[u8] {
        self.user_metadata()
    }
    fn remove_segments(&mut self, segment_ids: &[SegmentId]) {
        self.remove_segments(segment_ids)
    }
}

/// Load metadata from the best available root pointer.
///
/// Returns `(snapshot, used_inactive)` where `used_inactive` is true if the
/// active root was invalid and we fell back to the inactive root.
#[cfg(unix)]
fn load_metadata(file: &File, header: &FileHeader) -> Result<(MetadataSnapshot, bool)> {
    // Try active root.
    if let Some(snap) = try_load_root(file, header.active_root_pointer())? {
        return Ok((snap, false));
    }

    // Try inactive root.
    if let Some(snap) = try_load_root(file, header.inactive_root_pointer())? {
        return Ok((snap, true));
    }

    // Both roots empty → fresh (never committed) file.
    if !header.active_root_pointer().is_populated()
        && !header.inactive_root_pointer().is_populated()
    {
        return Ok((MetadataSnapshot::empty(), false));
    }

    // At least one root was populated but both failed validation.
    Err(LuciError::IndexCorrupted(
        "both root pointers failed checksum validation".into(),
    ))
}

/// Try to load metadata from a single root pointer.
///
/// Returns `None` if the root is empty or its checksum doesn't match.
#[cfg(unix)]
fn try_load_root(file: &File, root: &RootPointer) -> Result<Option<MetadataSnapshot>> {
    let block_id = match root.block_id {
        Some(id) => id,
        None => return Ok(None),
    };

    let mut block_buf = vec![0u8; BLOCK_SIZE as usize];
    file.read_exact_at(&mut block_buf, block_id.byte_offset())?;

    let computed = xxh3_checksum(&block_buf);
    if computed != root.checksum {
        return Ok(None);
    }

    let snap = MetadataSnapshot::from_bytes(&block_buf)?;
    Ok(Some(snap))
}

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

    /// Create a temp directory for test files. Returns the path.
    fn test_dir() -> std::path::PathBuf {
        let dir = std::env::temp_dir().join(format!("luci_test_{}", std::process::id()));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    fn test_path(name: &str) -> std::path::PathBuf {
        test_dir().join(name)
    }

    #[test]
    fn create_new_file() {
        let path = test_path("create_new.luci");
        let _ = fs::remove_file(&path);

        let dir = SingleFileDirectory::create(&path).unwrap();
        assert!(dir.segments().is_empty());
        assert_eq!(dir.generation(), 0);

        // File should exist with at least the header.
        let meta = fs::metadata(&path).unwrap();
        assert!(meta.len() >= HEADER_SIZE as u64);

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn create_fails_if_exists() {
        let path = test_path("create_exists.luci");
        let _ = fs::remove_file(&path);

        let _dir = SingleFileDirectory::create(&path).unwrap();
        let err = SingleFileDirectory::create(&path);
        assert!(err.is_err());

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn write_commit_read() {
        let path = test_path("write_commit_read.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();

        let data = b"hello luci segment data!";
        dir.write_segment(SegmentId::new(1), data).unwrap();
        dir.commit().unwrap();

        let read_back = dir.read_segment(SegmentId::new(1)).unwrap();
        assert_eq!(read_back, data);
        assert_eq!(dir.segments().len(), 1);
        assert_eq!(dir.generation(), 1);

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn multiple_segments() {
        let path = test_path("multiple_segments.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();

        dir.write_segment(SegmentId::new(1), b"segment-one").unwrap();
        dir.write_segment(SegmentId::new(2), b"segment-two-longer").unwrap();
        dir.commit().unwrap();

        assert_eq!(dir.segments().len(), 2);
        assert_eq!(
            dir.read_segment(SegmentId::new(1)).unwrap(),
            b"segment-one"
        );
        assert_eq!(
            dir.read_segment(SegmentId::new(2)).unwrap(),
            b"segment-two-longer"
        );

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn reopen_after_commit() {
        let path = test_path("reopen.luci");
        let _ = fs::remove_file(&path);

        // Create and write.
        {
            let mut dir = SingleFileDirectory::create(&path).unwrap();
            dir.write_segment(SegmentId::new(1), b"persistent data")
                .unwrap();
            dir.commit().unwrap();
        }

        // Reopen and read.
        {
            let dir = SingleFileDirectory::open(&path).unwrap();
            assert_eq!(dir.segments().len(), 1);
            assert_eq!(
                dir.read_segment(SegmentId::new(1)).unwrap(),
                b"persistent data"
            );
            assert_eq!(dir.generation(), 1);
        }

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn uncommitted_writes_not_visible_after_reopen() {
        let path = test_path("uncommitted.luci");
        let _ = fs::remove_file(&path);

        {
            let mut dir = SingleFileDirectory::create(&path).unwrap();
            dir.write_segment(SegmentId::new(1), b"committed").unwrap();
            dir.commit().unwrap();

            // Write but don't commit.
            dir.write_segment(SegmentId::new(2), b"uncommitted")
                .unwrap();
        }

        {
            let dir = SingleFileDirectory::open(&path).unwrap();
            assert_eq!(dir.segments().len(), 1);
            assert_eq!(
                dir.read_segment(SegmentId::new(1)).unwrap(),
                b"committed"
            );
            assert!(dir.read_segment(SegmentId::new(2)).is_err());
        }

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn multiple_commits() {
        let path = test_path("multi_commit.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();

        dir.write_segment(SegmentId::new(1), b"first").unwrap();
        dir.commit().unwrap();
        assert_eq!(dir.generation(), 1);

        dir.write_segment(SegmentId::new(2), b"second").unwrap();
        dir.commit().unwrap();
        assert_eq!(dir.generation(), 2);

        dir.write_segment(SegmentId::new(3), b"third").unwrap();
        dir.commit().unwrap();
        assert_eq!(dir.generation(), 3);

        assert_eq!(dir.segments().len(), 3);
        assert_eq!(dir.read_segment(SegmentId::new(1)).unwrap(), b"first");
        assert_eq!(dir.read_segment(SegmentId::new(2)).unwrap(), b"second");
        assert_eq!(dir.read_segment(SegmentId::new(3)).unwrap(), b"third");

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn reopen_after_multiple_commits() {
        let path = test_path("reopen_multi.luci");
        let _ = fs::remove_file(&path);

        {
            let mut dir = SingleFileDirectory::create(&path).unwrap();
            dir.write_segment(SegmentId::new(1), b"aaa").unwrap();
            dir.commit().unwrap();
            dir.write_segment(SegmentId::new(2), b"bbb").unwrap();
            dir.commit().unwrap();
            dir.write_segment(SegmentId::new(3), b"ccc").unwrap();
            dir.commit().unwrap();
        }

        {
            let dir = SingleFileDirectory::open(&path).unwrap();
            assert_eq!(dir.segments().len(), 3);
            assert_eq!(dir.read_segment(SegmentId::new(1)).unwrap(), b"aaa");
            assert_eq!(dir.read_segment(SegmentId::new(2)).unwrap(), b"bbb");
            assert_eq!(dir.read_segment(SegmentId::new(3)).unwrap(), b"ccc");
            assert_eq!(dir.generation(), 3);
        }

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn large_segment_spanning_multiple_blocks() {
        let path = test_path("large_segment.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();

        // Write data larger than one block (256 KB).
        let data = vec![0xABu8; BLOCK_SIZE as usize * 3 + 1000];
        dir.write_segment(SegmentId::new(1), &data).unwrap();
        dir.commit().unwrap();

        let read_back = dir.read_segment(SegmentId::new(1)).unwrap();
        assert_eq!(read_back, data);

        // Verify it allocated 4 blocks (3 full + 1 partial).
        assert_eq!(dir.segments()[0].extent.count, 4);

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn read_nonexistent_segment_is_error() {
        let path = test_path("read_nonexist.luci");
        let _ = fs::remove_file(&path);

        let dir = SingleFileDirectory::create(&path).unwrap();
        let err = dir.read_segment(SegmentId::new(999));
        assert!(err.is_err());

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn empty_commit_is_noop() {
        let path = test_path("empty_commit.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();
        // Committing with no pending segments should be a no-op.
        dir.commit().unwrap();
        assert_eq!(dir.generation(), 0);
        assert!(dir.segments().is_empty());

        fs::remove_file(&path).unwrap();
    }

    #[test]
    fn simulated_torn_header_falls_back() {
        let path = test_path("torn_header.luci");
        let _ = fs::remove_file(&path);

        // Create file with two commits so both roots are populated.
        {
            let mut dir = SingleFileDirectory::create(&path).unwrap();
            dir.write_segment(SegmentId::new(1), b"first-commit")
                .unwrap();
            dir.commit().unwrap();
            dir.write_segment(SegmentId::new(2), b"second-commit")
                .unwrap();
            dir.commit().unwrap();
        }

        // Corrupt the active root's checksum in the header to simulate a
        // torn header write.
        {
            let file = OpenOptions::new()
                .read(true)
                .write(true)
                .open(&path)
                .unwrap();
            let mut header_buf = [0u8; HEADER_SIZE as usize];
            file.read_exact_at(&mut header_buf, 0).unwrap();
            let header = FileHeader::from_bytes(&header_buf).unwrap();

            // Corrupt the active root's checksum.
            let checksum_offset = match header.active_root {
                crate::ActiveRoot::A => 24usize, // OFF_ROOT_A_CHECKSUM
                crate::ActiveRoot::B => 40usize, // OFF_ROOT_B_CHECKSUM
            };
            header_buf[checksum_offset] ^= 0xFF;
            file.write_all_at(&header_buf, 0).unwrap();
            file.sync_all().unwrap();
        }

        // Open should recover using the inactive root (first commit).
        {
            let dir = SingleFileDirectory::open(&path).unwrap();
            assert_eq!(dir.segments().len(), 1);
            assert_eq!(
                dir.read_segment(SegmentId::new(1)).unwrap(),
                b"first-commit"
            );
            // Second commit's segment should not be visible.
            assert!(dir.read_segment(SegmentId::new(2)).is_err());
        }

        fs::remove_file(&path).unwrap();
    }
}
