// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{
    collections::HashMap,
    fs::Metadata,
    path::{Path, PathBuf},
    sync::{Arc, Mutex, OnceLock},
    time::{Duration, Instant},
};
use tokio::{
    fs::{self, File},
    io,
};

static READONLY_OPTIONS: OnceLock<Arc<fs::OpenOptions>> = OnceLock::new();
static WRITEONLY_OPTIONS: OnceLock<Arc<fs::OpenOptions>> = OnceLock::new();
static READWRITE_OPTIONS: OnceLock<Arc<fs::OpenOptions>> = OnceLock::new();

fn get_readonly_options() -> &'static Arc<fs::OpenOptions> {
    READONLY_OPTIONS.get_or_init(|| {
        let mut opts = fs::OpenOptions::new();
        opts.read(true);
        Arc::new(opts)
    })
}

fn get_writeonly_options() -> &'static Arc<fs::OpenOptions> {
    WRITEONLY_OPTIONS.get_or_init(|| {
        let mut opts = fs::OpenOptions::new();
        opts.write(true);
        Arc::new(opts)
    })
}

fn get_readwrite_options() -> &'static Arc<fs::OpenOptions> {
    READWRITE_OPTIONS.get_or_init(|| {
        let mut opts = fs::OpenOptions::new();
        opts.read(true).write(true);
        Arc::new(opts)
    })
}

#[cfg(not(windows))]
pub fn same_file(f1: &Metadata, f2: &Metadata) -> bool {
    use std::os::unix::fs::MetadataExt;

    if f1.dev() != f2.dev() {
        return false;
    }

    if f1.ino() != f2.ino() {
        return false;
    }

    if f1.size() != f2.size() {
        return false;
    }
    if f1.permissions() != f2.permissions() {
        return false;
    }

    if f1.mtime() != f2.mtime() {
        return false;
    }

    true
}

#[cfg(windows)]
pub fn same_file(f1: &Metadata, f2: &Metadata) -> bool {
    if f1.permissions() != f2.permissions() {
        return false;
    }

    if f1.file_type() != f2.file_type() {
        return false;
    }

    if f1.len() != f2.len() {
        return false;
    }
    true
}

type FileMode = usize;

pub const O_RDONLY: FileMode = 0x00000;
pub const O_WRONLY: FileMode = 0x00001;
pub const O_RDWR: FileMode = 0x00002;
pub const O_CREATE: FileMode = 0x00040;
// pub const O_EXCL: FileMode = 0x00080;
// pub const O_NOCTTY: FileMode = 0x00100;
pub const O_TRUNC: FileMode = 0x00200;
// pub const O_NONBLOCK: FileMode = 0x00800;
pub const O_APPEND: FileMode = 0x00400;
// pub const O_SYNC: FileMode = 0x01000;
// pub const O_ASYNC: FileMode = 0x02000;
// pub const O_CLOEXEC: FileMode = 0x80000;

//      read: bool,
//     write: bool,
//     append: bool,
//     truncate: bool,
//     create: bool,
//     create_new: bool,

pub async fn open_file(path: impl AsRef<Path>, mode: FileMode) -> io::Result<File> {
    let base_opts = match mode & (O_RDONLY | O_WRONLY | O_RDWR) {
        O_RDONLY => get_readonly_options(),
        O_WRONLY => get_writeonly_options(),
        O_RDWR => get_readwrite_options(),
        _ => get_readonly_options(),
    };

    if (mode & (O_CREATE | O_APPEND | O_TRUNC)) != 0 {
        let mut opts = (**base_opts).clone();
        if mode & O_CREATE != 0 {
            opts.create(true);
        }
        if mode & O_APPEND != 0 {
            opts.append(true);
        }
        if mode & O_TRUNC != 0 {
            opts.truncate(true);
        }
        opts.open(path.as_ref()).await
    } else {
        base_opts.open(path.as_ref()).await
    }
}

pub async fn access(path: impl AsRef<Path>) -> io::Result<()> {
    fs::metadata(path).await?;
    Ok(())
}

pub fn access_std(path: impl AsRef<Path>) -> io::Result<()> {
    std::fs::metadata(path)?;
    Ok(())
}

pub async fn lstat(path: impl AsRef<Path>) -> io::Result<Metadata> {
    fs::symlink_metadata(path).await
}

pub fn lstat_std(path: impl AsRef<Path>) -> io::Result<Metadata> {
    std::fs::symlink_metadata(path)
}

pub async fn make_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
    fs::create_dir_all(path.as_ref()).await
}

fn is_dir_error(e: &io::Error) -> bool {
    e.raw_os_error() == Some(libc::EISDIR)
        || e.kind() == io::ErrorKind::IsADirectory
        // macOS: remove_file on a directory returns EPERM
        || (cfg!(target_os = "macos") && e.raw_os_error() == Some(libc::EPERM))
}

#[tracing::instrument(level = "debug", skip_all)]
pub async fn remove(path: impl AsRef<Path>) -> io::Result<()> {
    // Try remove_file first; fall back to remove_dir if it's a directory
    match fs::remove_file(path.as_ref()).await {
        Ok(()) => Ok(()),
        Err(e) if is_dir_error(&e) => fs::remove_dir(path.as_ref()).await,
        Err(e) => Err(e),
    }
}

#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
pub async fn remove_all(path: impl AsRef<Path>) -> io::Result<()> {
    // Try remove_file first; fall back to remove_dir_all if it's a directory
    match fs::remove_file(path.as_ref()).await {
        Ok(()) => Ok(()),
        Err(e) if is_dir_error(&e) => fs::remove_dir_all(path.as_ref()).await,
        Err(e) => Err(e),
    }
}

#[tracing::instrument(level = "debug", skip_all)]
pub fn remove_std(path: impl AsRef<Path>) -> io::Result<()> {
    // Try remove_file first; fall back to remove_dir if it's a directory
    match std::fs::remove_file(path.as_ref()) {
        Ok(()) => Ok(()),
        Err(e) if is_dir_error(&e) => std::fs::remove_dir(path.as_ref()),
        Err(e) => Err(e),
    }
}

pub fn remove_all_std(path: impl AsRef<Path>) -> io::Result<()> {
    let path = path.as_ref();
    let meta = std::fs::metadata(path)?;
    if meta.is_dir() {
        std::fs::remove_dir_all(path)
    } else {
        std::fs::remove_file(path)
    }
}

pub async fn mkdir(path: impl AsRef<Path>) -> io::Result<()> {
    fs::create_dir(path.as_ref()).await
}

pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
    fs::rename(from, to).await
}

pub fn rename_std(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
    std::fs::rename(from, to)
}

#[tracing::instrument(level = "debug", skip_all)]
pub async fn read_file(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
    fs::read(path.as_ref()).await
}

// Bucket existence cache - reduces statx syscalls for repeated bucket checks

/// Cache for bucket directory existence checks.
struct BucketExistenceCache {
    cache: Mutex<HashMap<PathBuf, (Instant, bool)>>,
    ttl: Duration,
}

impl BucketExistenceCache {
    fn new(ttl: Duration) -> Self {
        Self {
            cache: Mutex::new(HashMap::new()),
            ttl,
        }
    }

    fn check_exists(&self, path: &PathBuf) -> Option<bool> {
        let mut cache = self.cache.lock().ok()?;
        if let Some((timestamp, exists)) = cache.get(path) {
            if timestamp.elapsed() < self.ttl {
                return Some(*exists);
            }
            cache.remove(path);
        }
        None
    }

    fn record(&self, path: PathBuf, exists: bool) {
        if let Ok(mut cache) = self.cache.lock() {
            cache.insert(path, (Instant::now(), exists));
        }
    }

    fn invalidate(&self, path: &PathBuf) {
        if let Ok(mut cache) = self.cache.lock() {
            cache.remove(path);
        }
    }
}

static BUCKET_EXISTENCE_CACHE: std::sync::LazyLock<BucketExistenceCache> =
    std::sync::LazyLock::new(|| BucketExistenceCache::new(Duration::from_secs(60)));

/// Cached access check - reduces statx syscalls
pub async fn cached_access(path: impl AsRef<Path>) -> io::Result<()> {
    let path_buf = path.as_ref().to_path_buf();

    if let Some(exists) = BUCKET_EXISTENCE_CACHE.check_exists(&path_buf) {
        if exists {
            return Ok(());
        }
        return Err(io::Error::new(io::ErrorKind::NotFound, "bucket not found (cached)"));
    }

    let result = fs::metadata(&path_buf).await;

    match &result {
        Ok(_) => BUCKET_EXISTENCE_CACHE.record(path_buf, true),
        Err(e) if e.kind() == io::ErrorKind::NotFound => {
            BUCKET_EXISTENCE_CACHE.record(path_buf, false);
        }
        _ => {}
    }

    result?;
    Ok(())
}

pub fn invalidate_bucket_cache(path: impl AsRef<Path>) {
    BUCKET_EXISTENCE_CACHE.invalidate(&path.as_ref().to_path_buf());
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    use tokio::io::AsyncWriteExt;

    #[tokio::test]
    async fn test_file_mode_constants() {
        assert_eq!(O_RDONLY, 0x00000);
        assert_eq!(O_WRONLY, 0x00001);
        assert_eq!(O_RDWR, 0x00002);
        assert_eq!(O_CREATE, 0x00040);
        assert_eq!(O_TRUNC, 0x00200);
        assert_eq!(O_APPEND, 0x00400);
    }

    #[tokio::test]
    async fn test_open_file_read_only() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_readonly.txt");

        // Create a test file
        tokio::fs::write(&file_path, b"test content").await.unwrap();

        // Test opening in read-only mode
        let file = open_file(&file_path, O_RDONLY).await;
        assert!(file.is_ok());
    }

    #[tokio::test]
    async fn test_open_file_write_only() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_writeonly.txt");

        // Test opening in write-only mode with create flag
        let mut file = open_file(&file_path, O_WRONLY | O_CREATE).await.unwrap();

        // Should be able to write
        file.write_all(b"write test").await.unwrap();
        file.flush().await.unwrap();
    }

    #[tokio::test]
    async fn test_open_file_read_write() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_readwrite.txt");

        // Test opening in read-write mode with create flag
        let mut file = open_file(&file_path, O_RDWR | O_CREATE).await.unwrap();

        // Should be able to write and read
        file.write_all(b"read-write test").await.unwrap();
        file.flush().await.unwrap();
    }

    #[tokio::test]
    async fn test_open_file_append() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_append.txt");

        // Create initial content
        tokio::fs::write(&file_path, b"initial").await.unwrap();

        // Open in append mode
        let mut file = open_file(&file_path, O_WRONLY | O_APPEND).await.unwrap();
        file.write_all(b" appended").await.unwrap();
        file.flush().await.unwrap();

        // Verify content
        let content = tokio::fs::read_to_string(&file_path).await.unwrap();
        assert_eq!(content, "initial appended");
    }

    #[tokio::test]
    async fn test_open_file_truncate() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_truncate.txt");

        // Create initial content
        tokio::fs::write(&file_path, b"initial content").await.unwrap();

        // Open with truncate flag
        let mut file = open_file(&file_path, O_WRONLY | O_TRUNC).await.unwrap();
        file.write_all(b"new").await.unwrap();
        file.flush().await.unwrap();

        // Verify content was truncated
        let content = tokio::fs::read_to_string(&file_path).await.unwrap();
        assert_eq!(content, "new");
    }

    #[tokio::test]
    async fn test_access() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_access.txt");

        // Should fail for non-existent file
        assert!(access(&file_path).await.is_err());

        // Create file and test again
        tokio::fs::write(&file_path, b"test").await.unwrap();
        assert!(access(&file_path).await.is_ok());
    }

    #[test]
    fn test_access_std() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_access_std.txt");

        // Should fail for non-existent file
        assert!(access_std(&file_path).is_err());

        // Create file and test again
        std::fs::write(&file_path, b"test").unwrap();
        assert!(access_std(&file_path).is_ok());
    }

    #[tokio::test]
    async fn test_lstat() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_lstat.txt");

        // Create test file
        tokio::fs::write(&file_path, b"test content").await.unwrap();

        // Test lstat
        let metadata = lstat(&file_path).await.unwrap();
        assert!(metadata.is_file());
        assert_eq!(metadata.len(), 12); // "test content" is 12 bytes
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn test_lstat_preserves_symlink_metadata() {
        use std::os::unix::fs::symlink;

        let temp_dir = TempDir::new().unwrap();
        let target_path = temp_dir.path().join("target.txt");
        let link_path = temp_dir.path().join("link.txt");

        tokio::fs::write(&target_path, b"test content").await.unwrap();
        symlink(&target_path, &link_path).unwrap();

        let metadata = lstat(&link_path).await.unwrap();
        assert!(metadata.file_type().is_symlink());
    }

    #[test]
    fn test_lstat_std() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_lstat_std.txt");

        // Create test file
        std::fs::write(&file_path, b"test content").unwrap();

        // Test lstat_std
        let metadata = lstat_std(&file_path).unwrap();
        assert!(metadata.is_file());
        assert_eq!(metadata.len(), 12); // "test content" is 12 bytes
    }

    #[cfg(unix)]
    #[test]
    fn test_lstat_std_preserves_symlink_metadata() {
        use std::os::unix::fs::symlink;

        let temp_dir = TempDir::new().unwrap();
        let target_path = temp_dir.path().join("target-std.txt");
        let link_path = temp_dir.path().join("link-std.txt");

        std::fs::write(&target_path, b"test content").unwrap();
        symlink(&target_path, &link_path).unwrap();

        let metadata = lstat_std(&link_path).unwrap();
        assert!(metadata.file_type().is_symlink());
    }

    #[tokio::test]
    async fn test_make_dir_all() {
        let temp_dir = TempDir::new().unwrap();
        let nested_path = temp_dir.path().join("level1").join("level2").join("level3");

        // Should create nested directories
        assert!(make_dir_all(&nested_path).await.is_ok());
        assert!(nested_path.exists());
        assert!(nested_path.is_dir());
    }

    #[tokio::test]
    async fn test_remove_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_remove.txt");

        // Create test file
        tokio::fs::write(&file_path, b"test").await.unwrap();
        assert!(file_path.exists());

        // Remove file
        assert!(remove(&file_path).await.is_ok());
        assert!(!file_path.exists());
    }

    #[tokio::test]
    async fn test_remove_directory() {
        let temp_dir = TempDir::new().unwrap();
        let dir_path = temp_dir.path().join("test_remove_dir");

        // Create test directory
        tokio::fs::create_dir(&dir_path).await.unwrap();
        assert!(dir_path.exists());

        // Remove directory
        assert!(remove(&dir_path).await.is_ok());
        assert!(!dir_path.exists());
    }

    #[tokio::test]
    async fn test_remove_all() {
        let temp_dir = TempDir::new().unwrap();
        let dir_path = temp_dir.path().join("test_remove_all");
        let file_path = dir_path.join("nested_file.txt");

        // Create nested structure
        tokio::fs::create_dir(&dir_path).await.unwrap();
        tokio::fs::write(&file_path, b"nested content").await.unwrap();

        // Remove all
        assert!(remove_all(&dir_path).await.is_ok());
        assert!(!dir_path.exists());
    }

    #[test]
    fn test_remove_std() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_remove_std.txt");

        // Create test file
        std::fs::write(&file_path, b"test").unwrap();
        assert!(file_path.exists());

        // Remove file
        assert!(remove_std(&file_path).is_ok());
        assert!(!file_path.exists());
    }

    #[test]
    fn test_remove_all_std() {
        let temp_dir = TempDir::new().unwrap();
        let dir_path = temp_dir.path().join("test_remove_all_std");
        let file_path = dir_path.join("nested_file.txt");

        // Create nested structure
        std::fs::create_dir(&dir_path).unwrap();
        std::fs::write(&file_path, b"nested content").unwrap();

        // Remove all
        assert!(remove_all_std(&dir_path).is_ok());
        assert!(!dir_path.exists());
    }

    #[tokio::test]
    async fn test_mkdir() {
        let temp_dir = TempDir::new().unwrap();
        let dir_path = temp_dir.path().join("test_mkdir");

        // Create directory
        assert!(mkdir(&dir_path).await.is_ok());
        assert!(dir_path.exists());
        assert!(dir_path.is_dir());
    }

    #[tokio::test]
    async fn test_rename() {
        let temp_dir = TempDir::new().unwrap();
        let old_path = temp_dir.path().join("old_name.txt");
        let new_path = temp_dir.path().join("new_name.txt");

        // Create test file
        tokio::fs::write(&old_path, b"test content").await.unwrap();
        assert!(old_path.exists());
        assert!(!new_path.exists());

        // Rename file
        assert!(rename(&old_path, &new_path).await.is_ok());
        assert!(!old_path.exists());
        assert!(new_path.exists());

        // Verify content preserved
        let content = tokio::fs::read_to_string(&new_path).await.unwrap();
        assert_eq!(content, "test content");
    }

    #[test]
    fn test_rename_std() {
        let temp_dir = TempDir::new().unwrap();
        let old_path = temp_dir.path().join("old_name_std.txt");
        let new_path = temp_dir.path().join("new_name_std.txt");

        // Create test file
        std::fs::write(&old_path, b"test content").unwrap();
        assert!(old_path.exists());
        assert!(!new_path.exists());

        // Rename file
        assert!(rename_std(&old_path, &new_path).is_ok());
        assert!(!old_path.exists());
        assert!(new_path.exists());

        // Verify content preserved
        let content = std::fs::read_to_string(&new_path).unwrap();
        assert_eq!(content, "test content");
    }

    #[tokio::test]
    async fn test_read_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_read.txt");

        let test_content = b"This is test content for reading";
        tokio::fs::write(&file_path, test_content).await.unwrap();

        // Read file
        let read_content = read_file(&file_path).await.unwrap();
        assert_eq!(read_content, test_content);
    }

    #[tokio::test]
    async fn test_read_file_nonexistent() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("nonexistent.txt");

        // Should fail for non-existent file
        assert!(read_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_same_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_same.txt");

        // Create test file
        tokio::fs::write(&file_path, b"test content").await.unwrap();

        // Get metadata twice
        let metadata1 = tokio::fs::metadata(&file_path).await.unwrap();
        let metadata2 = tokio::fs::metadata(&file_path).await.unwrap();

        // Should be the same file
        assert!(same_file(&metadata1, &metadata2));
    }

    #[tokio::test]
    async fn test_different_files() {
        let temp_dir = TempDir::new().unwrap();
        let file1_path = temp_dir.path().join("file1.txt");
        let file2_path = temp_dir.path().join("file2.txt");

        // Create two different files
        tokio::fs::write(&file1_path, b"content1").await.unwrap();
        tokio::fs::write(&file2_path, b"different content").await.unwrap();

        // Get metadata
        let metadata1 = tokio::fs::metadata(&file1_path).await.unwrap();
        let metadata2 = tokio::fs::metadata(&file2_path).await.unwrap();

        // Should be different files
        assert!(!same_file(&metadata1, &metadata2));
    }
}
