use std::collections::HashMap;
use std::time::SystemTime;

/// A node in the virtual filesystem tree.
///
/// Generic over `F`, the user-defined metadata type. Both files and
/// directories can carry metadata. Auto-created ancestor directories get `None`.
#[derive(Debug, Clone)]
pub enum VfsEntry<F> {
    File(F),
    Directory {
        children: Vec<String>,
        meta: Option<F>,
    },
}

impl<F> VfsEntry<F> {
    pub fn is_file(&self) -> bool {
        matches!(self, VfsEntry::File(_))
    }

    pub fn is_dir(&self) -> bool {
        matches!(self, VfsEntry::Directory { .. })
    }

    pub fn as_file(&self) -> Option<&F> {
        match self {
            VfsEntry::File(f) => Some(f),
            _ => None,
        }
    }

    pub fn children(&self) -> Option<&[String]> {
        match self {
            VfsEntry::Directory { children, .. } => Some(children),
            _ => None,
        }
    }

    pub fn meta(&self) -> Option<&F> {
        match self {
            VfsEntry::File(f) => Some(f),
            VfsEntry::Directory { meta, .. } => meta.as_ref(),
        }
    }
}

/// Metadata for a VFS entry. Implement on your metadata type `F` so
/// [`VfsTree`] can produce `VfsMetadata` automatically.
///
/// Only `len` is required; timestamp methods default to `None`.
pub trait Metadata {
    fn len(&self) -> u64;

    fn created(&self) -> Option<SystemTime> {
        None
    }
    fn modified(&self) -> Option<SystemTime> {
        None
    }
    fn accessed(&self) -> Option<SystemTime> {
        None
    }
}

pub fn system_time_from_unix(secs: i64) -> SystemTime {
    if secs >= 0 {
        std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64)
    } else {
        std::time::UNIX_EPOCH - std::time::Duration::from_secs((-secs) as u64)
    }
}

#[cfg(feature = "jiff")]
pub fn system_time_from_jiff(ts: &jiff::Timestamp) -> SystemTime {
    let secs = ts.as_second();
    let nanos = ts.subsec_nanosecond();
    if secs >= 0 {
        std::time::UNIX_EPOCH
            + std::time::Duration::from_secs(secs as u64)
            + std::time::Duration::from_nanos(nanos as u64)
    } else {
        let abs_secs = (-secs) as u64;
        let base = std::time::UNIX_EPOCH - std::time::Duration::from_secs(abs_secs);
        if nanos > 0 {
            base + std::time::Duration::from_nanos(nanos as u64)
        } else {
            base
        }
    }
}

/// Read-only virtual filesystem tree backed by a `HashMap`.
///
/// The root is represented by an empty string. Construct via [`VfsTreeBuilder`].
#[derive(Debug, Clone)]
pub struct VfsTree<F> {
    entries: HashMap<String, VfsEntry<F>>,
}

impl<F> VfsTree<F> {
    pub fn builder() -> VfsTreeBuilder<F> {
        VfsTreeBuilder::new()
    }

    /// Normalizes empty and "/" paths to the root. Strips leading `/`.
    pub fn lookup(&self, path: &str) -> Option<&VfsEntry<F>> {
        let key = normalize_path(path);
        self.entries.get(key)
    }

    pub fn exists(&self, path: &str) -> bool {
        self.lookup(path).is_some()
    }

    pub fn iter(&self) -> impl Iterator<Item = (&str, &VfsEntry<F>)> {
        self.entries.iter().map(|(k, v)| (k.as_str(), v))
    }

    pub fn read_dir(&self, path: &str) -> Option<&[String]> {
        self.lookup(path).and_then(|e| e.children())
    }
}

fn empty_dir<F>() -> VfsEntry<F> {
    VfsEntry::Directory {
        children: Vec::new(),
        meta: None,
    }
}

/// Builder for constructing a [`VfsTree`].
///
/// Inserting a file automatically creates all ancestor directories and
/// registers the file as a child of its parent.
#[derive(Debug)]
pub struct VfsTreeBuilder<F> {
    entries: HashMap<String, VfsEntry<F>>,
}

impl<F> VfsTreeBuilder<F> {
    pub fn new() -> Self {
        let mut entries = HashMap::new();
        entries.insert(String::new(), empty_dir());
        Self { entries }
    }

    /// All ancestor directories are created automatically.
    pub fn insert(mut self, path: impl Into<String>, file_meta: F) -> Self {
        let path = path.into();
        let path = path.strip_prefix('/').unwrap_or(&path).to_string();

        let parts: Vec<&str> = path.split('/').collect();
        for depth in 0..parts.len() {
            let parent_path = if depth == 0 {
                String::new()
            } else {
                parts[..depth].join("/")
            };
            let child_name = parts[depth];

            let parent = self.entries.entry(parent_path).or_insert_with(empty_dir);

            if let VfsEntry::Directory { children, .. } = parent
                && !children.contains(&child_name.to_string())
            {
                children.push(child_name.to_string());
            }

            if depth < parts.len() - 1 {
                let dir_path = parts[..=depth].join("/");
                self.entries.entry(dir_path).or_insert_with(empty_dir);
            }
        }

        self.entries.insert(path, VfsEntry::File(file_meta));
        self
    }

    /// Ancestor directories are created automatically. If the directory
    /// already exists and `meta` is `Some`, the metadata is updated.
    pub fn insert_dir(mut self, path: impl Into<String>, meta: Option<F>) -> Self {
        let path = path.into();
        let path = path.strip_prefix('/').unwrap_or(&path).to_string();

        if path.is_empty() {
            if let Some(m) = meta
                && let Some(VfsEntry::Directory { meta: slot, .. }) = self.entries.get_mut("")
            {
                *slot = Some(m);
            }
            return self;
        }

        let parts: Vec<&str> = path.split('/').collect();
        for depth in 0..parts.len() {
            let parent_path = if depth == 0 {
                String::new()
            } else {
                parts[..depth].join("/")
            };
            let child_name = parts[depth];

            let parent = self.entries.entry(parent_path).or_insert_with(empty_dir);

            if let VfsEntry::Directory { children, .. } = parent
                && !children.contains(&child_name.to_string())
            {
                children.push(child_name.to_string());
            }

            let dir_path = parts[..=depth].join("/");
            if depth < parts.len() - 1 {
                self.entries.entry(dir_path).or_insert_with(empty_dir);
            } else {
                match self.entries.get_mut(&dir_path) {
                    Some(VfsEntry::Directory { meta: slot, .. }) => {
                        if meta.is_some() {
                            *slot = meta;
                        }
                        return self;
                    }
                    None => {
                        self.entries.insert(
                            dir_path,
                            VfsEntry::Directory {
                                children: Vec::new(),
                                meta,
                            },
                        );
                        return self;
                    }
                    _ => {}
                }
            }
        }

        self
    }

    pub fn build(self) -> VfsTree<F> {
        VfsTree {
            entries: self.entries,
        }
    }
}

impl<F> Default for VfsTreeBuilder<F> {
    fn default() -> Self {
        Self::new()
    }
}

fn normalize_path(path: &str) -> &str {
    path.strip_prefix('/').unwrap_or(path)
}

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

    #[derive(Debug, Clone)]
    struct TestMeta {
        size: u64,
    }

    impl Metadata for TestMeta {
        fn len(&self) -> u64 {
            self.size
        }
    }

    #[test]
    fn build_tree_creates_ancestors() {
        let tree = VfsTree::builder()
            .insert("a/b/c.txt", TestMeta { size: 100 })
            .insert("a/b/d.txt", TestMeta { size: 200 })
            .insert("a/e.txt", TestMeta { size: 50 })
            .build();

        assert!(tree.lookup("").unwrap().is_dir());
        assert!(tree.lookup("/").unwrap().is_dir());
        assert!(tree.lookup("a").unwrap().is_dir());
        assert!(tree.lookup("a/b").unwrap().is_dir());
        assert!(tree.lookup("a/b/c.txt").unwrap().is_file());
        assert!(tree.lookup("a/b/d.txt").unwrap().is_file());
        assert!(tree.lookup("a/e.txt").unwrap().is_file());

        assert_eq!(
            tree.lookup("a/b/c.txt").unwrap().as_file().unwrap().size,
            100
        );

        let root_children = tree.read_dir("").unwrap();
        assert_eq!(root_children, &["a"]);

        let a_children = tree.read_dir("a").unwrap();
        assert!(a_children.contains(&"b".to_string()));
        assert!(a_children.contains(&"e.txt".to_string()));

        let ab_children = tree.read_dir("a/b").unwrap();
        assert!(ab_children.contains(&"c.txt".to_string()));
        assert!(ab_children.contains(&"d.txt".to_string()));
    }

    #[test]
    fn exists_and_missing() {
        let tree = VfsTree::builder()
            .insert("foo/bar.txt", TestMeta { size: 10 })
            .build();

        assert!(tree.exists("foo/bar.txt"));
        assert!(tree.exists("foo"));
        assert!(tree.exists(""));
        assert!(!tree.exists("missing"));
        assert!(!tree.exists("foo/missing.txt"));
    }

    #[test]
    fn leading_slash_normalization() {
        let tree = VfsTree::builder()
            .insert("/hello/world.txt", TestMeta { size: 42 })
            .build();

        assert!(tree.exists("hello/world.txt"));
        assert!(tree.exists("/hello/world.txt"));
    }

    #[test]
    fn explicit_empty_dir() {
        let tree: VfsTree<TestMeta> = VfsTree::builder().insert_dir("empty/dir", None).build();

        assert!(tree.exists("empty"));
        assert!(tree.exists("empty/dir"));
        assert!(tree.lookup("empty/dir").unwrap().is_dir());
        assert_eq!(tree.read_dir("empty/dir").unwrap().len(), 0);
    }

    #[test]
    fn dir_with_metadata() {
        let tree = VfsTree::builder()
            .insert("dir/file.txt", TestMeta { size: 10 })
            .insert_dir("dir", Some(TestMeta { size: 0 }))
            .build();

        assert!(tree.lookup("dir").unwrap().meta().is_some());
        assert_eq!(tree.lookup("dir").unwrap().meta().unwrap().size, 0);
        assert!(tree.lookup("").unwrap().meta().is_none());
    }

    #[test]
    fn system_time_from_unix_positive() {
        let st = system_time_from_unix(1_700_000_000);
        let dur = st.duration_since(std::time::UNIX_EPOCH).unwrap();
        assert_eq!(dur.as_secs(), 1_700_000_000);
    }

    #[test]
    fn system_time_from_unix_zero() {
        let st = system_time_from_unix(0);
        assert_eq!(st, std::time::UNIX_EPOCH);
    }

    #[test]
    fn system_time_from_unix_negative() {
        let st = system_time_from_unix(-100);
        let dur = std::time::UNIX_EPOCH.duration_since(st).unwrap();
        assert_eq!(dur.as_secs(), 100);
    }

    #[test]
    fn metadata_defaults_to_none() {
        struct SizeOnly;
        impl Metadata for SizeOnly {
            fn len(&self) -> u64 {
                42
            }
        }

        let m = SizeOnly;
        assert_eq!(m.len(), 42);
        assert!(m.created().is_none());
        assert!(m.modified().is_none());
        assert!(m.accessed().is_none());
    }
}

#[cfg(all(test, feature = "jiff"))]
mod jiff_tests {
    use super::*;

    #[test]
    fn jiff_timestamp_to_system_time() {
        let ts = jiff::Timestamp::from_second(1_700_000_000).unwrap();
        let st = system_time_from_jiff(&ts);
        let dur = st.duration_since(std::time::UNIX_EPOCH).unwrap();
        assert_eq!(dur.as_secs(), 1_700_000_000);
        assert_eq!(dur.subsec_nanos(), 0);
    }

    #[test]
    fn jiff_timestamp_with_nanos() {
        let ts = jiff::Timestamp::new(1_700_000_000, 500_000_000).unwrap();
        let st = system_time_from_jiff(&ts);
        let dur = st.duration_since(std::time::UNIX_EPOCH).unwrap();
        assert_eq!(dur.as_secs(), 1_700_000_000);
        assert_eq!(dur.subsec_nanos(), 500_000_000);
    }

    #[test]
    fn jiff_epoch_zero() {
        let ts = jiff::Timestamp::UNIX_EPOCH;
        let st = system_time_from_jiff(&ts);
        assert_eq!(st, std::time::UNIX_EPOCH);
    }
}

#[cfg(all(test, feature = "vfs"))]
mod vfs_tests {
    use super::*;
    use std::io::Cursor;
    use vfs::FileSystem;

    use crate::vfs_impl::ReadOnlyVfs;

    #[derive(Debug, Clone)]
    struct FileMeta {
        data: Vec<u8>,
    }

    impl Metadata for FileMeta {
        fn len(&self) -> u64 {
            self.data.len() as u64
        }
    }

    #[derive(Debug)]
    struct InMemoryOpener;

    impl crate::vfs_impl::FileOpener<FileMeta> for InMemoryOpener {
        fn open(&self, meta: &FileMeta) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>> {
            Ok(Box::new(Cursor::new(meta.data.clone())))
        }
    }

    #[test]
    fn read_only_vfs() {
        use std::io::Read;

        let tree = VfsTree::builder()
            .insert(
                "docs/readme.txt",
                FileMeta {
                    data: b"hello world".to_vec(),
                },
            )
            .insert(
                "docs/license.txt",
                FileMeta {
                    data: b"MIT".to_vec(),
                },
            )
            .build();

        let fs = ReadOnlyVfs::new(tree, InMemoryOpener);

        let children: Vec<String> = fs.read_dir("").unwrap().collect();
        assert_eq!(children, vec!["docs"]);

        let doc_children: Vec<String> = fs.read_dir("docs").unwrap().collect();
        assert!(doc_children.contains(&"readme.txt".to_string()));
        assert!(doc_children.contains(&"license.txt".to_string()));

        let mut file = fs.open_file("docs/readme.txt").unwrap();
        let mut buf = String::new();
        file.read_to_string(&mut buf).unwrap();
        assert_eq!(buf, "hello world");

        let meta = fs.metadata("docs/readme.txt").unwrap();
        assert_eq!(meta.file_type, vfs::VfsFileType::File);
        assert_eq!(meta.len, 11);

        let dir_meta = fs.metadata("docs").unwrap();
        assert_eq!(dir_meta.file_type, vfs::VfsFileType::Directory);

        assert!(fs.exists("docs/readme.txt").unwrap());
        assert!(!fs.exists("missing").unwrap());

        assert!(fs.create_dir("foo").is_err());
        assert!(fs.remove_file("docs/readme.txt").is_err());
    }

    #[test]
    fn vfs_metadata_timestamps() {
        #[derive(Debug, Clone)]
        struct TimedMeta {
            size: u64,
            mtime: Option<SystemTime>,
        }

        impl Metadata for TimedMeta {
            fn len(&self) -> u64 {
                self.size
            }
            fn modified(&self) -> Option<SystemTime> {
                self.mtime
            }
        }

        let file_mtime = system_time_from_unix(1_700_000_000);
        let dir_mtime = system_time_from_unix(1_600_000_000);
        let tree = VfsTree::builder()
            .insert(
                "dir/file.txt",
                TimedMeta {
                    size: 42,
                    mtime: Some(file_mtime),
                },
            )
            .insert_dir(
                "dir",
                Some(TimedMeta {
                    size: 0,
                    mtime: Some(dir_mtime),
                }),
            )
            .build();

        let meta = tree.vfs_metadata("dir/file.txt").unwrap();
        assert_eq!(meta.len, 42);
        assert_eq!(meta.modified, Some(file_mtime));
        assert_eq!(meta.created, None);

        let dir_meta = tree.vfs_metadata("dir").unwrap();
        assert_eq!(dir_meta.file_type, vfs::VfsFileType::Directory);
        assert_eq!(dir_meta.modified, Some(dir_mtime));
        assert_eq!(dir_meta.created, None);

        let root_meta = tree.vfs_metadata("").unwrap();
        assert_eq!(root_meta.modified, None);
    }
}
