//! [`LocalFileSystem`] — minimal [`FileSystem`] adapter over [`tokio::fs`].
//!
//! Maps [`RemotePath`] (a location-neutral string path) onto the local OS
//! filesystem. Only the operations needed to wire up the composition root and
//! later lots are implemented; richer behaviour (and SSH/WSL siblings) arrives
//! in L2/L9.

use std::io;
use std::path::Path;

use async_trait::async_trait;
use domain::ports::{DirEntry, FileSystem, FsError, RemotePath};
use tokio::fs;

/// Filesystem adapter backed by the local OS via `tokio::fs`.
#[derive(Debug, Default, Clone, Copy)]
pub struct LocalFileSystem;

impl LocalFileSystem {
    /// Creates a new [`LocalFileSystem`].
    #[must_use]
    pub const fn new() -> Self {
        Self
    }
}

/// Maps a [`std::io::Error`] to the domain's [`FsError`], preserving the
/// not-found / permission distinctions the application layer cares about.
fn map_io(path: &RemotePath, err: &io::Error) -> FsError {
    match err.kind() {
        io::ErrorKind::NotFound => FsError::NotFound(path.as_str().to_owned()),
        io::ErrorKind::PermissionDenied => FsError::PermissionDenied(path.as_str().to_owned()),
        _ => FsError::Io(format!("{}: {err}", path.as_str())),
    }
}

#[async_trait]
impl FileSystem for LocalFileSystem {
    async fn read(&self, path: &RemotePath) -> Result<Vec<u8>, FsError> {
        fs::read(path.as_str()).await.map_err(|e| map_io(path, &e))
    }

    async fn write(&self, path: &RemotePath, data: &[u8]) -> Result<(), FsError> {
        fs::write(path.as_str(), data)
            .await
            .map_err(|e| map_io(path, &e))
    }

    async fn exists(&self, path: &RemotePath) -> Result<bool, FsError> {
        match fs::metadata(path.as_str()).await {
            Ok(_) => Ok(true),
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
            Err(e) => Err(map_io(path, &e)),
        }
    }

    async fn remove_file(&self, path: &RemotePath) -> Result<(), FsError> {
        match fs::remove_file(path.as_str()).await {
            Ok(()) => Ok(()),
            // Idempotent best-effort delete: an already-absent file is success.
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
            Err(e) => Err(map_io(path, &e)),
        }
    }

    async fn create_dir_all(&self, path: &RemotePath) -> Result<(), FsError> {
        fs::create_dir_all(path.as_str())
            .await
            .map_err(|e| map_io(path, &e))
    }

    async fn list(&self, path: &RemotePath) -> Result<Vec<DirEntry>, FsError> {
        let mut entries = Vec::new();
        let mut read_dir = fs::read_dir(path.as_str())
            .await
            .map_err(|e| map_io(path, &e))?;

        while let Some(entry) = read_dir.next_entry().await.map_err(|e| map_io(path, &e))? {
            let is_dir = entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false);
            entries.push(DirEntry {
                name: entry.file_name().to_string_lossy().into_owned(),
                is_dir,
            });
        }
        Ok(entries)
    }

    async fn symlink(&self, src: &RemotePath, dst: &RemotePath) -> Result<(), FsError> {
        symlink_impl(Path::new(src.as_str()), Path::new(dst.as_str()))
            .await
            .map_err(|e| map_io(dst, &e))
    }
}

#[cfg(unix)]
async fn symlink_impl(src: &Path, dst: &Path) -> io::Result<()> {
    fs::symlink(src, dst).await
}

#[cfg(windows)]
async fn symlink_impl(src: &Path, dst: &Path) -> io::Result<()> {
    // On Windows we default to a file symlink; directory symlinks require a
    // different call and are handled when the store layer (L2/L6) needs them.
    fs::symlink_file(src, dst).await
}
