use memmap2::Mmap;
use std::fs::File;
use std::io::Read;
use std::path::Path;

/// Threshold for using memory-mapped I/O (1 MB).
const MMAP_THRESHOLD: u64 = 1024 * 1024;

/// Size of the sample checked by `is_binary` (8 KB).
const BINARY_CHECK_SIZE: usize = 8192;

/// Read a file's contents as a string.
///
/// Uses memory-mapped I/O for large files, regular reads for small ones.
pub fn read_file(path: &Path) -> std::io::Result<String> {
    let metadata = std::fs::metadata(path)?;
    let size = metadata.len();

    if size >= MMAP_THRESHOLD {
        read_mmap(path)
    } else {
        read_regular(path)
    }
}

fn read_regular(path: &Path) -> std::io::Result<String> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

fn read_mmap(path: &Path) -> std::io::Result<String> {
    let file = File::open(path)?;
    // SAFETY: The mapping is immediately copied into an owned Vec via `.to_vec()`,
    // so the mmap is live for only the duration of that copy. A concurrent writer
    // using rename-based atomic writes (as ripsed does) cannot affect the mapped
    // inode during this window. The resulting Vec is a stable snapshot.
    let mmap = unsafe { Mmap::map(&file)? };
    String::from_utf8(mmap.to_vec())
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}

/// Check if a file appears to be binary by looking for null bytes in the
/// first 8 KB of the file.
pub fn is_binary(path: &Path) -> std::io::Result<bool> {
    let mut file = File::open(path)?;
    let mut buffer = [0u8; BINARY_CHECK_SIZE];
    let bytes_read = file.read(&mut buffer)?;
    Ok(buffer[..bytes_read].contains(&0))
}

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

    // ---- Binary detection tests ----

    #[test]
    fn is_binary_detects_null_bytes() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("bin.dat");
        fs::write(&path, b"\x00\x01\x02\x03").unwrap();

        assert!(is_binary(&path).unwrap());
    }

    #[test]
    fn is_binary_text_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("text.txt");
        fs::write(&path, "just text\n").unwrap();

        assert!(!is_binary(&path).unwrap());
    }

    #[test]
    fn is_binary_null_at_position_after_512() {
        // Ensures we check beyond the old 512-byte boundary
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("sneaky.bin");
        let mut data = vec![b'A'; 1000];
        data[999] = 0; // null byte at position 999
        fs::write(&path, &data).unwrap();

        assert!(is_binary(&path).unwrap());
    }

    #[test]
    fn is_binary_null_just_inside_8kb() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("edge.bin");
        let mut data = vec![b'X'; BINARY_CHECK_SIZE];
        data[BINARY_CHECK_SIZE - 1] = 0;
        fs::write(&path, &data).unwrap();

        assert!(is_binary(&path).unwrap());
    }

    #[test]
    fn is_binary_null_just_past_8kb() {
        // Null byte at position 8192 is beyond our check window
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("past.bin");
        let mut data = vec![b'X'; BINARY_CHECK_SIZE + 1];
        data[BINARY_CHECK_SIZE] = 0;
        fs::write(&path, &data).unwrap();

        assert!(!is_binary(&path).unwrap());
    }

    #[test]
    fn is_binary_empty_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("empty.bin");
        fs::write(&path, "").unwrap();

        assert!(!is_binary(&path).unwrap());
    }

    // ---- read_file tests ----

    #[test]
    fn read_file_small() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("small.txt");
        fs::write(&path, "hello").unwrap();

        assert_eq!(read_file(&path).unwrap(), "hello");
    }

    #[test]
    fn read_file_large_uses_mmap_path() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("large.txt");
        // Create a file larger than MMAP_THRESHOLD (1 MB + 1 byte)
        let content = "x".repeat(MMAP_THRESHOLD as usize + 1);
        fs::write(&path, &content).unwrap();

        let result = read_file(&path).unwrap();
        assert_eq!(result.len(), MMAP_THRESHOLD as usize + 1);
        assert_eq!(result, content);
    }
}
