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

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

use crate::block::{BlockId, Extent};
use crate::directory::{MetadataSnapshot, SegmentEntry};

/// Debug/development storage backend: one file per segment on the filesystem.
///
/// Each segment is stored as `segment_{id}.bin` in the root directory.
/// Committed metadata is persisted as `metadata.json`. This backend has no
/// atomic commit guarantees — it exists for inspecting segment contents during
/// development and for property-testing against [`SingleFileDirectory`].
///
/// See [[architecture-storage-format]] for the production backend.
///
/// [`SingleFileDirectory`]: crate::SingleFileDirectory
pub struct FsDirectory {
    root: PathBuf,
    /// Last committed metadata — the source of truth for reads.
    committed: MetadataSnapshot,
    /// Segments written but not yet committed.
    pending_segments: Vec<PendingSegment>,
    /// Segments to remove on next commit.
    pending_removals: Vec<SegmentId>,
    /// Monotonically increasing commit counter.
    generation: u64,
}

struct PendingSegment {
    segment_id: SegmentId,
    data_len: u64,
}

const METADATA_FILE: &str = "metadata.json";

impl FsDirectory {
    /// Create a new `FsDirectory` at the given path.
    ///
    /// Creates the directory and writes an initial empty `metadata.json`.
    /// Fails if a `metadata.json` already exists at the path.
    pub fn create(path: impl AsRef<Path>) -> Result<Self> {
        let root = path.as_ref().to_path_buf();
        fs::create_dir_all(&root)?;

        let metadata_path = root.join(METADATA_FILE);
        if metadata_path.exists() {
            return Err(LuciError::InvalidQuery(
                "FsDirectory already exists".into(),
            ));
        }

        let dir = Self {
            root,
            committed: MetadataSnapshot::empty(),
            pending_segments: Vec::new(),
            pending_removals: Vec::new(),
            generation: 0,
        };

        dir.write_metadata()?;
        Ok(dir)
    }

    /// Open an existing `FsDirectory`.
    ///
    /// Reads `metadata.json` to reconstruct the committed state.
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        let root = path.as_ref().to_path_buf();
        let metadata_path = root.join(METADATA_FILE);

        let json_str = fs::read_to_string(&metadata_path)?;
        let (committed, generation) = parse_metadata(&json_str)?;

        Ok(Self {
            root,
            committed,
            pending_segments: Vec::new(),
            pending_removals: Vec::new(),
            generation,
        })
    }

    /// Write segment data to a file.
    ///
    /// The segment is not visible to readers until [`commit`](Self::commit)
    /// is called. Data is written to disk immediately as `segment_{id}.bin`.
    ///
    /// # 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 path = self.segment_path(segment_id);
        fs::write(&path, data)?;

        self.pending_segments.push(PendingSegment {
            segment_id,
            data_len: 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 path = self.segment_path(segment_id);
        let data = fs::read(&path)?;

        debug_assert_eq!(data.len() as u64, entry.data_len);
        Ok(data)
    }

    /// Commit all pending segment writes by updating `metadata.json`.
    ///
    /// No atomic commit guarantees — this is a simple file write.
    pub fn commit(&mut self) -> Result<()> {
        if self.pending_segments.is_empty() && self.pending_removals.is_empty() {
            return Ok(());
        }

        self.generation += 1;

        let mut segments = self.committed.segments.clone();
        for pending in self.pending_segments.drain(..) {
            segments.push(SegmentEntry::new(
                pending.segment_id,
                Extent::new(BlockId(0), 0), // FsDirectory doesn't use blocks
                self.generation,
                pending.data_len,
            ));
        }

        // Remove merged source segments and delete their files.
        if !self.pending_removals.is_empty() {
            segments.retain(|entry| {
                if self.pending_removals.contains(&entry.segment_id) {
                    let _ = std::fs::remove_file(self.segment_path(entry.segment_id));
                    false
                } else {
                    true
                }
            });
            self.pending_removals.clear();
        }

        self.committed = MetadataSnapshot {
            segments,
            total_blocks: 0,
            free_list: Vec::new(),
            user_metadata: self.committed.user_metadata.clone(),
        };

        self.write_metadata()?;
        Ok(())
    }

    /// 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);
    }

    /// 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
    }

    fn segment_path(&self, segment_id: SegmentId) -> PathBuf {
        self.root
            .join(format!("segment_{}.bin", segment_id.as_u64()))
    }

    fn write_metadata(&self) -> Result<()> {
        let json = serialize_metadata(&self.committed, self.generation);
        fs::write(self.root.join(METADATA_FILE), json)?;
        Ok(())
    }
}

impl crate::Storage for FsDirectory {
    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)
    }
}

fn serialize_metadata(snapshot: &MetadataSnapshot, generation: u64) -> String {
    let segments: Vec<serde_json::Value> = snapshot
        .segments
        .iter()
        .map(|e| {
            serde_json::json!({
                "segment_id": e.segment_id.as_u64(),
                "generation": e.generation,
                "data_len": e.data_len,
            })
        })
        .collect();

    let mut value = serde_json::json!({
        "generation": generation,
        "segments": segments,
    });

    // Store user metadata as a JSON string (the blob is UTF-8 JSON from the index layer)
    if !snapshot.user_metadata.is_empty() {
        if let Ok(s) = std::str::from_utf8(&snapshot.user_metadata) {
            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(s) {
                value["user_metadata"] = parsed;
            }
        }
    }

    serde_json::to_string_pretty(&value).unwrap()
}

fn parse_metadata(json: &str) -> Result<(MetadataSnapshot, u64)> {
    let value: serde_json::Value = serde_json::from_str(json)
        .map_err(|e| LuciError::IndexCorrupted(format!("invalid metadata JSON: {e}")))?;

    let generation = value["generation"]
        .as_u64()
        .ok_or_else(|| LuciError::IndexCorrupted("missing generation".into()))?;

    let segments_array = value["segments"]
        .as_array()
        .ok_or_else(|| LuciError::IndexCorrupted("missing segments array".into()))?;

    let mut segments = Vec::with_capacity(segments_array.len());
    for seg in segments_array {
        let segment_id = seg["segment_id"]
            .as_u64()
            .ok_or_else(|| LuciError::IndexCorrupted("missing segment_id".into()))?;
        let seg_gen = seg["generation"]
            .as_u64()
            .ok_or_else(|| LuciError::IndexCorrupted("missing generation".into()))?;
        let data_len = seg["data_len"]
            .as_u64()
            .ok_or_else(|| LuciError::IndexCorrupted("missing data_len".into()))?;

        segments.push(SegmentEntry::new(
            SegmentId::new(segment_id),
            Extent::new(BlockId(0), 0),
            seg_gen,
            data_len,
        ));
    }

    // User metadata (may be absent in old format)
    let user_metadata = value.get("user_metadata")
        .map(|v| serde_json::to_vec(v).unwrap_or_default())
        .unwrap_or_default();

    Ok((
        MetadataSnapshot {
            segments,
            total_blocks: 0,
            free_list: Vec::new(),
            user_metadata,
        },
        generation,
    ))
}

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

    fn test_dir() -> PathBuf {
        let dir = std::env::temp_dir().join(format!("luci_fsdir_test_{}", std::process::id()));
        stdfs::create_dir_all(&dir).unwrap();
        dir
    }

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

    fn cleanup(path: &Path) {
        let _ = stdfs::remove_dir_all(path);
    }

    #[test]
    fn create_new_directory() {
        let path = test_path("create_new");
        cleanup(&path);

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

        cleanup(&path);
    }

    #[test]
    fn create_fails_if_exists() {
        let path = test_path("create_exists");
        cleanup(&path);

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

        cleanup(&path);
    }

    #[test]
    fn write_commit_read() {
        let path = test_path("write_commit_read");
        cleanup(&path);

        let mut dir = FsDirectory::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);

        cleanup(&path);
    }

    #[test]
    fn multiple_segments() {
        let path = test_path("multi_segments");
        cleanup(&path);

        let mut dir = FsDirectory::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"
        );

        cleanup(&path);
    }

    #[test]
    fn reopen_after_commit() {
        let path = test_path("reopen");
        cleanup(&path);

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

        {
            let dir = FsDirectory::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);
        }

        cleanup(&path);
    }

    #[test]
    fn uncommitted_writes_not_visible_after_reopen() {
        let path = test_path("uncommitted");
        cleanup(&path);

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

        {
            let dir = FsDirectory::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());
        }

        cleanup(&path);
    }

    #[test]
    fn multiple_commits() {
        let path = test_path("multi_commit");
        cleanup(&path);

        let mut dir = FsDirectory::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");

        cleanup(&path);
    }

    #[test]
    fn empty_commit_is_noop() {
        let path = test_path("empty_commit");
        cleanup(&path);

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

        cleanup(&path);
    }

    #[test]
    fn read_nonexistent_segment_is_error() {
        let path = test_path("read_nonexist");
        cleanup(&path);

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

        cleanup(&path);
    }

    #[test]
    fn segment_files_exist_on_disk() {
        let path = test_path("files_on_disk");
        cleanup(&path);

        let mut dir = FsDirectory::create(&path).unwrap();
        dir.write_segment(SegmentId::new(42), b"inspectable")
            .unwrap();
        dir.commit().unwrap();

        // Segment file should exist for manual inspection.
        assert!(path.join("segment_42.bin").exists());

        // Metadata JSON should be human-readable.
        let meta = stdfs::read_to_string(path.join(METADATA_FILE)).unwrap();
        assert!(meta.contains("\"segment_id\": 42"));

        cleanup(&path);
    }

    #[test]
    fn reopen_after_multiple_commits() {
        let path = test_path("reopen_multi");
        cleanup(&path);

        {
            let mut dir = FsDirectory::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 = FsDirectory::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);
        }

        cleanup(&path);
    }

    #[test]
    fn write_empty_segment_is_error() {
        let path = test_path("write_empty");
        cleanup(&path);

        let mut dir = FsDirectory::create(&path).unwrap();
        let err = dir.write_segment(SegmentId::new(1), b"");
        assert!(err.is_err());

        cleanup(&path);
    }
}
