//! POSIX-compatible filesystem wrapper.
//!
//! This module provides [`PosixFs`], a wrapper that adds POSIX-like semantics
//! on top of any [`FsBackend`] implementation.
//!
//! # Overview
//!
//! `PosixFs` takes a simple storage backend and adds:
//!
//! | Check | Description |
//! |-------|-------------|
//! | Type-safe writes | `write_file` fails with "is a directory" if path is a directory |
//! | Type-safe mkdir | `mkdir` fails with "file exists" if path is a file |
//! | Parent directory | Write operations require parent directory to exist |
//! | read_dir validation | Fails if path is not a directory |
//!
//! # Example
//!
//! ```rust,ignore
//! use bashkit::{Bash, FsBackend, PosixFs};
//! use std::sync::Arc;
//!
//! // 1. Implement FsBackend for your storage
//! struct MyStorage { /* ... */ }
//! impl FsBackend for MyStorage { /* ... */ }
//!
//! // 2. Wrap with PosixFs
//! let backend = MyStorage::new();
//! let fs = Arc::new(PosixFs::new(backend));
//!
//! // 3. Use with Bash
//! let mut bash = Bash::builder().fs(fs).build();
//!
//! // POSIX semantics are automatically enforced:
//! bash.exec("mkdir /tmp/dir").await?;
//! let result = bash.exec("echo test > /tmp/dir 2>&1").await?;
//! // ^ This fails with "is a directory"
//! ```
//!
//! # When to Use
//!
//! Use `PosixFs` when:
//! - You have a simple storage backend that doesn't enforce POSIX rules
//! - You want automatic type checking without implementing it yourself
//! - You're bridging to an external storage system (database, cloud, etc.)
//!
//! See [`FsBackend`](super::FsBackend) for how to implement a backend.

use async_trait::async_trait;
use std::io::Error as IoError;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;

use super::backend::FsBackend;
use super::limits::{FsLimits, FsUsage};
use super::normalize_path;
use super::traits::{DirEntry, FileSystem, FileSystemExt, Metadata, fs_errors};
use crate::error::Result;

/// POSIX-compatible filesystem wrapper.
///
/// Wraps any [`FsBackend`] and enforces POSIX-like semantics.
///
/// # Semantics Enforced
///
/// | Operation | Check |
/// |-----------|-------|
/// | `write_file` | Fails if path is a directory |
/// | `append_file` | Fails if path is a directory |
/// | `mkdir` | Fails if path exists as file (always) or dir (unless recursive) |
/// | `read_dir` | Fails if path is not a directory |
/// | `copy` | Fails if source is a directory |
///
/// # Example
///
/// ```rust,ignore
/// use bashkit::{FsBackend, PosixFs, Bash};
/// use std::sync::Arc;
///
/// // Your simple storage backend
/// let backend = MyStorage::new();
///
/// // Wrap with PosixFs for POSIX semantics
/// let fs = Arc::new(PosixFs::new(backend));
///
/// // Use with Bash interpreter
/// let mut bash = Bash::builder().fs(fs).build();
/// ```
pub struct PosixFs<B: FsBackend> {
    backend: B,
}

impl<B: FsBackend> PosixFs<B> {
    /// Create a new POSIX-compatible filesystem wrapper.
    pub fn new(backend: B) -> Self {
        Self { backend }
    }

    /// Get a reference to the underlying backend.
    pub fn backend(&self) -> &B {
        &self.backend
    }

    /// Normalize a path for consistent lookups.
    fn normalize(path: &Path) -> PathBuf {
        normalize_path(path)
    }

    /// Check if parent directory exists.
    async fn check_parent_exists(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent()
            && parent != Path::new("/")
            && parent != Path::new("")
            && !self.backend.exists(parent).await?
        {
            return Err(fs_errors::parent_not_found());
        }
        Ok(())
    }
}

#[async_trait]
impl<B: FsBackend + 'static> FileSystem for PosixFs<B> {
    async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
        let path = Self::normalize(path);
        // Check if it's a directory
        if let Ok(meta) = self.backend.stat(&path).await
            && meta.file_type.is_dir()
        {
            return Err(fs_errors::is_a_directory());
        }
        self.backend.read(&path).await
    }

    async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
        let path = Self::normalize(path);
        // Check parent exists
        self.check_parent_exists(&path).await?;

        // Check if path is a directory
        if let Ok(meta) = self.backend.stat(&path).await
            && meta.file_type.is_dir()
        {
            return Err(fs_errors::is_a_directory());
        }

        self.backend.write(&path, content).await
    }

    async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
        let path = Self::normalize(path);
        // Check parent exists
        self.check_parent_exists(&path).await?;

        // Check if path is a directory
        if let Ok(meta) = self.backend.stat(&path).await
            && meta.file_type.is_dir()
        {
            return Err(fs_errors::is_a_directory());
        }

        self.backend.append(&path, content).await
    }

    async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
        let path = Self::normalize(path);
        // Check if something already exists at this path
        if let Ok(meta) = self.backend.stat(&path).await {
            if meta.file_type.is_dir() {
                // Directory exists
                if recursive {
                    return Ok(()); // mkdir -p on existing dir is OK
                } else {
                    return Err(fs_errors::already_exists("directory exists"));
                }
            } else {
                // File or symlink exists - always error
                return Err(fs_errors::already_exists("file exists"));
            }
        }

        if recursive {
            // Check each component in path for file conflicts
            if let Some(parent) = path.parent() {
                let mut current = PathBuf::from("/");
                for component in parent.components().skip(1) {
                    current.push(component);
                    if let Ok(meta) = self.backend.stat(&current).await
                        && !meta.file_type.is_dir()
                    {
                        return Err(fs_errors::already_exists("file exists"));
                    }
                }
            }
        } else {
            // Non-recursive: parent must exist
            self.check_parent_exists(&path).await?;
        }

        self.backend.mkdir(&path, recursive).await
    }

    async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
        let path = Self::normalize(path);
        self.backend.remove(&path, recursive).await
    }

    async fn stat(&self, path: &Path) -> Result<Metadata> {
        let path = Self::normalize(path);
        self.backend.stat(&path).await
    }

    async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
        let path = Self::normalize(path);
        // Check if it's actually a directory
        if let Ok(meta) = self.backend.stat(&path).await
            && !meta.file_type.is_dir()
        {
            return Err(fs_errors::not_a_directory());
        }
        self.backend.read_dir(&path).await
    }

    async fn exists(&self, path: &Path) -> Result<bool> {
        let path = Self::normalize(path);
        self.backend.exists(&path).await
    }

    async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
        let from = Self::normalize(from);
        let to = Self::normalize(to);
        self.backend.rename(&from, &to).await
    }

    async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
        let from = Self::normalize(from);
        let to = Self::normalize(to);
        // Check source is not a directory
        if let Ok(meta) = self.backend.stat(&from).await
            && meta.file_type.is_dir()
        {
            return Err(IoError::other("cannot copy directory").into());
        }
        self.backend.copy(&from, &to).await
    }

    async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
        // Don't normalize target: symlink targets are stored as-is on disk.
        // Normalizing a relative target to absolute would break containment checks.
        let link = Self::normalize(link);
        self.backend.symlink(target, &link).await
    }

    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
        let path = Self::normalize(path);
        self.backend.read_link(&path).await
    }

    async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
        let path = Self::normalize(path);
        self.backend.chmod(&path, mode).await
    }

    async fn set_modified_time(&self, path: &Path, time: SystemTime) -> Result<()> {
        let path = Self::normalize(path);
        self.backend.set_modified_time(&path, time).await
    }
}

#[async_trait]
impl<B: FsBackend + 'static> FileSystemExt for PosixFs<B> {
    fn usage(&self) -> FsUsage {
        self.backend.usage()
    }

    fn limits(&self) -> FsLimits {
        self.backend.limits()
    }
}

// Allow Arc<PosixFs<B>> to be used where Arc<dyn FileSystem> is expected
impl<B: FsBackend + 'static> From<PosixFs<B>> for Arc<dyn FileSystem> {
    fn from(fs: PosixFs<B>) -> Self {
        Arc::new(fs)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::Result;
    use crate::fs::InMemoryFs;
    use crate::fs::{DirEntry, FileType, FsBackend};
    use std::collections::HashSet;
    use std::path::{Path, PathBuf};
    use std::sync::Mutex;

    struct AppendCreatesFileBackend {
        files: Mutex<HashSet<PathBuf>>,
    }

    impl AppendCreatesFileBackend {
        fn new() -> Self {
            let mut files = HashSet::new();
            files.insert(PathBuf::from("/"));
            files.insert(PathBuf::from("/tmp"));
            Self {
                files: Mutex::new(files),
            }
        }
    }

    #[async_trait]
    impl FsBackend for AppendCreatesFileBackend {
        async fn read(&self, _path: &Path) -> Result<Vec<u8>> {
            Ok(Vec::new())
        }

        async fn write(&self, path: &Path, _content: &[u8]) -> Result<()> {
            self.files
                .lock()
                .expect("backend lock poisoned")
                .insert(path.to_path_buf());
            Ok(())
        }

        async fn append(&self, path: &Path, content: &[u8]) -> Result<()> {
            self.write(path, content).await
        }

        async fn mkdir(&self, path: &Path, _recursive: bool) -> Result<()> {
            self.files
                .lock()
                .expect("backend lock poisoned")
                .insert(path.to_path_buf());
            Ok(())
        }

        async fn remove(&self, _path: &Path, _recursive: bool) -> Result<()> {
            Ok(())
        }

        async fn stat(&self, path: &Path) -> Result<Metadata> {
            if self
                .files
                .lock()
                .expect("backend lock poisoned")
                .contains(path)
            {
                Ok(Metadata {
                    file_type: FileType::File,
                    ..Metadata::default()
                })
            } else {
                Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
            }
        }

        async fn read_dir(&self, _path: &Path) -> Result<Vec<DirEntry>> {
            Ok(Vec::new())
        }

        async fn exists(&self, path: &Path) -> Result<bool> {
            Ok(self
                .files
                .lock()
                .expect("backend lock poisoned")
                .contains(path))
        }

        async fn rename(&self, _from: &Path, _to: &Path) -> Result<()> {
            Ok(())
        }

        async fn copy(&self, _from: &Path, _to: &Path) -> Result<()> {
            Ok(())
        }

        async fn symlink(&self, _target: &Path, _link: &Path) -> Result<()> {
            Ok(())
        }

        async fn read_link(&self, _path: &Path) -> Result<PathBuf> {
            Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
        }

        async fn chmod(&self, _path: &Path, _mode: u32) -> Result<()> {
            Ok(())
        }
    }

    #[tokio::test]
    async fn test_posix_write_to_directory_fails() {
        // InMemoryFs already implements FileSystem with checks,
        // but we can test PosixFs wrapping a raw backend
        let fs = InMemoryFs::new();

        // Create a directory
        fs.mkdir(Path::new("/tmp/testdir"), false)
            .await
            .expect("mkdir should succeed");

        // Writing to it should fail
        let result = fs.write_file(Path::new("/tmp/testdir"), b"test").await;
        assert!(result.is_err());
        assert!(
            result
                .expect_err("write_file should fail")
                .to_string()
                .contains("directory")
        );
    }

    #[tokio::test]
    async fn test_posix_mkdir_on_file_fails() {
        let fs = InMemoryFs::new();

        // Create a file
        fs.write_file(Path::new("/tmp/testfile"), b"test")
            .await
            .expect("write_file should succeed");

        // mkdir on it should fail
        let result = fs.mkdir(Path::new("/tmp/testfile"), false).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_posix_normalize_dot_slash_prefix() {
        // Issue #1114: paths with ./ prefix should resolve correctly
        let fs = InMemoryFs::new();

        // Create /tmp/dir and a file
        fs.mkdir(Path::new("/tmp/dir"), true).await.unwrap();
        fs.write_file(Path::new("/tmp/dir/file.txt"), b"content")
            .await
            .unwrap();

        // Access via ./ style path (as if cwd.join("./file.txt"))
        let dot_path = Path::new("/tmp/dir/./file.txt");
        assert!(
            fs.exists(dot_path).await.unwrap(),
            "exists with ./ should work"
        );

        let content = fs.read_file(dot_path).await.unwrap();
        assert_eq!(content, b"content");

        // stat with ./ prefix
        let meta = fs.stat(dot_path).await;
        assert!(meta.is_ok(), "stat with ./ should work");

        // write via ./ prefix
        fs.write_file(Path::new("/tmp/dir/./new.txt"), b"new")
            .await
            .unwrap();
        let content = fs.read_file(Path::new("/tmp/dir/new.txt")).await.unwrap();
        assert_eq!(content, b"new");
    }

    #[tokio::test]
    async fn test_posix_normalize_preserves_semantics() {
        // Verify normalization doesn't break parent-exists checks
        let fs = InMemoryFs::new();

        // /tmp exists, /tmp/nonexistent does not
        let result = fs
            .write_file(Path::new("/tmp/nonexistent/./file.txt"), b"content")
            .await;
        assert!(result.is_err(), "should fail when parent doesn't exist");
    }

    #[tokio::test]
    async fn test_posix_append_requires_parent_directory() {
        let fs = PosixFs::new(AppendCreatesFileBackend::new());
        let result = fs
            .append_file(Path::new("/tmp/missing-parent/file.txt"), b"content")
            .await;
        assert!(
            result.is_err(),
            "append should fail when parent doesn't exist"
        );
    }
}
