//! File system IO operations — all return `Critical<T>`.
//!
//! This module wraps `std::fs` operations so that engine code never touches
//! `std::fs` directly. Every operation returns [`Critical<T>`] which must be
//! explicitly handled via `.suppress(token, "reason")` or `.suppress(token, "reason")`.

use std::path::Path;
use crate::critical::Critical;

/// Read the entire contents of a file into a String.
///
/// Wraps `std::fs::read_to_string`.
pub(crate) fn read_to_string(path: impl AsRef<Path>) -> Critical<String> {
    match std::fs::read_to_string(path.as_ref()) {
        Ok(s) => Critical::ok(s),
        Err(e) => Critical::error("fs::read_to_string", e.to_string()),
    }
}

/// Read the entire contents of a file into a byte vector.
///
/// Wraps `std::fs::read`.
pub(crate) fn read(path: impl AsRef<Path>) -> Critical<Vec<u8>> {
    match std::fs::read(path.as_ref()) {
        Ok(b) => Critical::ok(b),
        Err(e) => Critical::error("fs::read", e.to_string()),
    }
}

/// Write a slice as the entire contents of a file.
///
/// Wraps `std::fs::write`.
pub(crate) fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Critical<()> {
    match std::fs::write(path.as_ref(), contents.as_ref()) {
        Ok(()) => Critical::ok(()),
        Err(e) => Critical::error("fs::write", e.to_string()),
    }
}

/// Recursively create a directory and all of its parent components.
///
/// Wraps `std::fs::create_dir_all`.
pub(crate) fn create_dir_all(path: impl AsRef<Path>) -> Critical<()> {
    match std::fs::create_dir_all(path.as_ref()) {
        Ok(()) => Critical::ok(()),
        Err(e) => Critical::error("fs::create_dir_all", e.to_string()),
    }
}

/// Read the entries of a directory.
///
/// Wraps `std::fs::read_dir`. Returns collected entries to avoid
/// exposing the iterator (which would bypass Critical).
pub(crate) fn read_dir(path: impl AsRef<Path>) -> Critical<Vec<std::fs::DirEntry>> {
    match std::fs::read_dir(path.as_ref()) {
        Ok(iter) => {
            let mut entries = Vec::new();
            for entry in iter {
                match entry {
                    Ok(e) => entries.push(e),
                    Err(e) => return Critical::error("fs::read_dir", e.to_string()),
                }
            }
            Critical::ok(entries)
        }
        Err(e) => Critical::error("fs::read_dir", e.to_string()),
    }
}

/// Open a file with append mode, creating it if it doesn't exist.
///
/// Wraps `std::fs::OpenOptions` with `.create(true).append(true)`.
/// Returns a `std::fs::File` for streaming writes.
pub(crate) fn open_append(path: impl AsRef<Path>) -> Critical<std::fs::File> {
    match std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(path.as_ref())
    {
        Ok(f) => Critical::ok(f),
        Err(e) => Critical::error("fs::open_append", e.to_string()),
    }
}

/// Check if a path exists.
///
/// This is non-failing (returns bool directly, not Critical),
/// because checking existence is not an IO error scenario.
pub(crate) fn exists(path: impl AsRef<Path>) -> bool {
    path.as_ref().exists()
}

/// Remove a file.
///
/// Wraps `std::fs::remove_file`.
pub(crate) fn remove_file(path: impl AsRef<Path>) -> Critical<()> {
    match std::fs::remove_file(path.as_ref()) {
        Ok(()) => Critical::ok(()),
        Err(e) => Critical::error("fs::remove_file", e.to_string()),
    }
}

/// Rename a file or directory.
///
/// Wraps `std::fs::rename`.
pub(crate) fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Critical<()> {
    match std::fs::rename(from.as_ref(), to.as_ref()) {
        Ok(()) => Critical::ok(()),
        Err(e) => Critical::error("fs::rename", e.to_string()),
    }
}

/// Remove a directory and all its contents. Returns `Critical<()>`.
pub(crate) fn remove_dir_all(path: impl AsRef<Path>) -> Critical<()> {
    match std::fs::remove_dir_all(path.as_ref()) {
        Ok(()) => Critical::ok(()),
        Err(e) => Critical::error("fs::remove_dir_all", e.to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::critical::{ConceptMapToken, SuppressibleToken};

    #[derive(Debug, Clone, Copy, PartialEq)]
    enum TestToken { FsOp, FsSuppress }

    impl ConceptMapToken for TestToken {
        fn operation_name(&self) -> &'static str {
            match self {
                TestToken::FsOp => "test::fs_op",
                TestToken::FsSuppress => "test::fs_suppress",
            }
        }
    }

    impl SuppressibleToken for TestToken {}

    #[test]
    fn test_read_to_string_nonexistent() {
        let c = read_to_string("/nonexistent/path/file.txt");
        assert!(c.suppress(TestToken::FsOp, "test").is_none());
    }

    #[test]
    fn test_write_and_read() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.txt");
        write(&path, "hello").suppress(TestToken::FsOp, "test").unwrap();
        let content = read_to_string(&path).suppress(TestToken::FsOp, "test").unwrap();
        assert_eq!(content, "hello");
    }

    #[test]
    fn test_create_dir_all() {
        let dir = tempfile::tempdir().unwrap();
        let nested = dir.path().join("a").join("b").join("c");
        create_dir_all(&nested).suppress(TestToken::FsOp, "test").unwrap();
        assert!(nested.exists());
    }

    #[test]
    fn test_read_dir() {
        let dir = tempfile::tempdir().unwrap();
        write(dir.path().join("a.txt"), "a").suppress(TestToken::FsOp, "test").unwrap();
        write(dir.path().join("b.txt"), "b").suppress(TestToken::FsOp, "test").unwrap();
        let entries = read_dir(dir.path()).suppress(TestToken::FsOp, "test").unwrap();
        assert_eq!(entries.len(), 2);
    }

    #[test]
    fn test_exists() {
        assert!(!exists("/nonexistent/path"));
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("exists.txt");
        write(&path, "x").suppress(TestToken::FsOp, "test").unwrap();
        assert!(exists(&path));
    }

    #[test]
    fn test_suppress_nonexistent() {
        let content = read_to_string("/nonexistent").suppress(TestToken::FsSuppress, "optional file");
        assert!(content.is_none());
    }

    #[test]
    fn test_suppress_with_default() {
        let content = read_to_string("/nonexistent")
            .suppress_with(TestToken::FsSuppress, "using empty", String::new());
        assert_eq!(content, "");
    }

    #[test]
    fn test_open_append() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("append.txt");
        {
            let _f = open_append(&path).suppress(TestToken::FsOp, "test").unwrap();
        }
        assert!(path.exists());
    }

    #[test]
    fn test_remove_dir_all() {
        let dir = tempfile::tempdir().unwrap();
        let sub = dir.path().join("sub");
        create_dir_all(&sub).suppress(TestToken::FsOp, "test").unwrap();
        write(sub.join("file.txt"), "data").suppress(TestToken::FsOp, "test").unwrap();
        remove_dir_all(&sub).suppress(TestToken::FsOp, "test").unwrap();
        assert!(!sub.exists());
    }
}
