//! ## Remote Bridged Host
//!
//! Bridges a `RemoteFs` implementation behind the local host interface used by
//! the file transfer activity.

mod temp_mapped_file;

use std::io::{Read, Write};
use std::path::{Component, Path, PathBuf};

use remotefs::fs::{Metadata, UnixPex};
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};

use self::temp_mapped_file::TempMappedFile;
use super::{HostBridge, HostError, HostResult};
use crate::utils::path::normalize;

struct WriteStreamOp {
    path: PathBuf,
    metadata: Metadata,
    tempfile: TempMappedFile,
}

/// A remote host bridged over the local host
pub struct RemoteBridged {
    /// Remote fs client
    remote: Box<dyn RemoteFs>,
    /// Reminder used to finalize write stream
    write_stream_op: Option<WriteStreamOp>,
}

impl RemoteBridged {
    fn open_file_from_temp(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
        let mut temp_file = TempMappedFile::new()?;

        self.remote
            .open_file(file, Box::new(temp_file.clone()))
            .map_err(HostError::from)?;

        // Sync changes
        temp_file.sync()?;

        // now return as read
        Ok(Box::new(temp_file))
    }
}

impl From<Box<dyn RemoteFs>> for RemoteBridged {
    fn from(remote: Box<dyn RemoteFs>) -> Self {
        RemoteBridged {
            remote,
            write_stream_op: None,
        }
    }
}

impl HostBridge for RemoteBridged {
    fn connect(&mut self) -> HostResult<()> {
        self.remote.connect().map(|_| ()).map_err(HostError::from)
    }

    fn disconnect(&mut self) -> HostResult<()> {
        self.remote.disconnect().map_err(HostError::from)
    }

    fn is_connected(&mut self) -> bool {
        self.remote.is_connected()
    }

    fn is_localhost(&self) -> bool {
        false
    }

    fn pwd(&mut self) -> HostResult<PathBuf> {
        debug!("Getting working directory");
        self.remote.pwd().map_err(HostError::from)
    }

    fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {
        debug!("Changing working directory to {:?}", new_dir);
        self.remote.change_dir(new_dir).map_err(HostError::from)
    }

    fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> {
        debug!("Creating directory {:?}", dir_name);
        match self.remote.create_dir(dir_name, UnixPex::from(0o755)) {
            Ok(_) => Ok(()),
            Err(remotefs::RemoteError {
                kind: RemoteErrorType::DirectoryAlreadyExists,
                ..
            }) if ignore_existing => Ok(()),
            Err(e) => Err(HostError::from(e)),
        }
    }

    fn remove(&mut self, entry: &File) -> HostResult<()> {
        debug!("Removing {:?}", entry.path());
        if entry.is_dir() {
            self.remote
                .remove_dir_all(entry.path())
                .map_err(HostError::from)
        } else {
            self.remote
                .remove_file(entry.path())
                .map_err(HostError::from)
        }
    }

    fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> {
        debug!("Renaming {:?} to {:?}", entry.path(), dst_path);
        self.remote
            .mov(entry.path(), dst_path)
            .map_err(HostError::from)
    }

    fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> {
        debug!("Copying {:?} to {:?}", entry.path(), dst);
        self.remote.copy(entry.path(), dst).map_err(HostError::from)
    }

    fn stat(&mut self, path: &Path) -> HostResult<File> {
        debug!("Statting {:?}", path);
        self.remote.stat(path).map_err(HostError::from)
    }

    fn exists(&mut self, path: &Path) -> HostResult<bool> {
        debug!("Checking existence of {:?}", path);
        self.remote.exists(path).map_err(HostError::from)
    }

    fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
        debug!("Listing directory {:?}", path);
        let entries = self.remote.list_dir(path).map_err(HostError::from)?;
        Ok(filter_self_refs(path, entries))
    }

    fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
        debug!("Setting metadata for {:?}", path);
        self.remote
            .setstat(path, metadata.clone())
            .map_err(HostError::from)
    }

    fn exec(&mut self, cmd: &str) -> HostResult<String> {
        debug!("Executing command: {}", cmd);
        self.remote
            .exec(cmd)
            .map(|(_, stdout)| stdout)
            .map_err(HostError::from)
    }

    fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
        debug!("Creating symlink from {:?} to {:?}", src, dst);
        self.remote.symlink(src, dst).map_err(HostError::from)
    }

    fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> {
        debug!("Changing permissions of {:?} to {:?}", path, pex);
        let stat = self.remote.stat(path).map_err(HostError::from)?;
        let mut metadata = stat.metadata.clone();
        metadata.mode = Some(pex);

        self.setstat(path, &metadata)
    }

    fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
        // try to use stream, otherwise download to a temporary file and return a reader
        match self.remote.open(file) {
            Ok(stream) => Ok(Box::new(stream)),
            Err(RemoteError {
                kind: RemoteErrorType::UnsupportedFeature,
                ..
            }) => self.open_file_from_temp(file),
            Err(e) => Err(HostError::from(e)),
        }
    }

    fn create_file(
        &mut self,
        file: &Path,
        metadata: &Metadata,
    ) -> HostResult<Box<dyn Write + Send>> {
        // try to use stream, otherwise download to a temporary file and return a reader
        match self.remote.create(file, metadata) {
            Ok(stream) => Ok(Box::new(stream)),
            Err(RemoteError {
                kind: RemoteErrorType::UnsupportedFeature,
                ..
            }) => {
                let tempfile = TempMappedFile::new()?;
                self.write_stream_op = Some(WriteStreamOp {
                    path: file.to_path_buf(),
                    metadata: metadata.clone(),
                    tempfile: tempfile.clone(),
                });

                Ok(Box::new(tempfile))
            }
            Err(e) => Err(HostError::from(e)),
        }
    }

    fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
        if let Some(WriteStreamOp {
            path,
            metadata,
            mut tempfile,
        }) = self.write_stream_op.take()
        {
            // sync
            tempfile.sync()?;
            // write file
            self.remote
                .create_file(&path, &metadata, Box::new(tempfile))?;
        }
        Ok(())
    }
}

/// Drop entries that refer to the directory being listed.
///
/// Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
/// to the listed directory in the LIST response, which would otherwise appear
/// as a duplicate entry in the explorer.
fn filter_self_refs(path: &Path, entries: Vec<File>) -> Vec<File> {
    let normalized = normalize(path);
    entries
        .into_iter()
        .filter(|entry| {
            let last = entry.path().components().next_back();
            let is_dot_ref = matches!(last, Some(Component::CurDir | Component::ParentDir));
            !is_dot_ref && normalize(entry.path()) != normalized
        })
        .collect()
}

#[cfg(test)]
mod test {
    use std::path::PathBuf;
    use std::time::SystemTime;

    use pretty_assertions::assert_eq;
    use remotefs::fs::{FileType, Metadata};

    use super::*;

    fn file(path: &str, file_type: FileType) -> File {
        File {
            path: PathBuf::from(path),
            metadata: Metadata {
                accessed: Some(SystemTime::UNIX_EPOCH),
                created: Some(SystemTime::UNIX_EPOCH),
                modified: Some(SystemTime::UNIX_EPOCH),
                file_type,
                gid: None,
                mode: None,
                size: 0,
                symlink: None,
                uid: None,
            },
        }
    }

    #[test]
    fn filter_self_refs_drops_entry_matching_listed_dir() {
        let entries = vec![
            file("/wp-content/wp-content", FileType::Directory),
            file("/wp-content/index.php", FileType::File),
        ];

        let filtered = filter_self_refs(Path::new("/wp-content/wp-content"), entries);

        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].path(), Path::new("/wp-content/index.php"));
    }

    #[test]
    fn filter_self_refs_drops_dot_and_dotdot_entries() {
        let entries = vec![
            file("/foo/.", FileType::Directory),
            file("/foo/..", FileType::Directory),
            file("/foo/bar", FileType::File),
        ];

        let filtered = filter_self_refs(Path::new("/foo"), entries);

        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].path(), Path::new("/foo/bar"));
    }

    #[test]
    fn filter_self_refs_normalizes_paths() {
        let entries = vec![
            file("/foo/./bar", FileType::File),
            file("/foo/baz/../", FileType::Directory),
        ];

        let filtered = filter_self_refs(Path::new("/foo"), entries);

        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].path(), Path::new("/foo/./bar"));
    }

    #[test]
    fn filter_self_refs_preserves_unrelated_entries() {
        let entries = vec![
            file("/home/user/notes.txt", FileType::File),
            file("/home/user/photos", FileType::Directory),
        ];

        let filtered = filter_self_refs(Path::new("/home/user"), entries.clone());

        assert_eq!(filtered.len(), 2);
    }
}
