use crate::dirwrap::DirWrap;
use crate::filewrap::FileWrap;
use crate::sftp::types::{Metadata, OpenOptions, RenameOptions};
use crate::sftp::SftpChannelResult;
use camino::{Utf8Path, Utf8PathBuf};

pub(crate) enum SftpWrap {
    #[cfg(feature = "ssh2")]
    Ssh2(ssh2::Sftp),

    #[cfg(feature = "libssh-rs")]
    LibSsh(libssh_rs::Sftp),
}

#[cfg(feature = "ssh2")]
fn pathconv(path: std::path::PathBuf) -> SftpChannelResult<Utf8PathBuf> {
    use crate::sftp::SftpChannelError;
    use std::convert::TryFrom;
    Ok(Utf8PathBuf::try_from(path).map_err(|x| {
        SftpChannelError::from(std::io::Error::new(std::io::ErrorKind::InvalidData, x))
    })?)
}

impl SftpWrap {
    pub fn open(&self, filename: &Utf8Path, opts: OpenOptions) -> SftpChannelResult<FileWrap> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => {
                let flags: ssh2::OpenFlags = opts.into();
                let mode = opts.mode;
                let open_type: ssh2::OpenType = opts.ty.into();

                let file = sftp.open_mode(filename.as_std_path(), flags, mode, open_type)?;
                Ok(FileWrap::Ssh2(file))
            }

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => {
                use crate::sftp::types::WriteMode;
                use libc::{O_APPEND, O_RDONLY, O_RDWR, O_WRONLY};
                use std::convert::TryInto;
                let accesstype = match (opts.write, opts.read) {
                    (Some(WriteMode::Append), true) => O_RDWR | O_APPEND,
                    (Some(WriteMode::Append), false) => O_WRONLY | O_APPEND,
                    (Some(WriteMode::Write), false) => O_WRONLY,
                    (Some(WriteMode::Write), true) => O_RDWR,
                    (None, true) => O_RDONLY,
                    (None, false) => 0,
                };
                let file =
                    sftp.open(filename.as_str(), accesstype, opts.mode.try_into().unwrap())?;
                Ok(FileWrap::LibSsh(file))
            }
        }
    }

    pub fn symlink(&self, path: &Utf8Path, target: &Utf8Path) -> SftpChannelResult<()> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.symlink(path.as_std_path(), target.as_std_path())?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.symlink(path.as_str(), target.as_str())?),
        }
    }

    pub fn read_link(&self, filename: &Utf8Path) -> SftpChannelResult<Utf8PathBuf> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(pathconv(sftp.readlink(filename.as_std_path())?)?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.read_link(filename.as_str())?.into()),
        }
    }

    pub fn canonicalize(&self, filename: &Utf8Path) -> SftpChannelResult<Utf8PathBuf> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(pathconv(sftp.realpath(filename.as_std_path())?)?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.canonicalize(filename.as_str())?.into()),
        }
    }

    pub fn unlink(&self, filename: &Utf8Path) -> SftpChannelResult<()> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.unlink(filename.as_std_path())?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.remove_file(filename.as_str())?),
        }
    }

    pub fn remove_dir(&self, filename: &Utf8Path) -> SftpChannelResult<()> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.rmdir(filename.as_std_path())?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.remove_dir(filename.as_str())?),
        }
    }

    pub fn create_dir(&self, filename: &Utf8Path, mode: i32) -> SftpChannelResult<()> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.mkdir(filename.as_std_path(), mode)?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => {
                use std::convert::TryInto;
                Ok(sftp.create_dir(filename.as_str(), mode.try_into().unwrap())?)
            }
        }
    }

    pub fn rename(
        &self,
        src: &Utf8Path,
        dest: &Utf8Path,
        #[cfg_attr(not(feature = "ssh2"), allow(unused_variables))] opts: RenameOptions,
    ) -> SftpChannelResult<()> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => {
                Ok(sftp.rename(src.as_std_path(), dest.as_std_path(), Some(opts.into()))?)
            }

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.rename(src.as_str(), dest.as_str())?),
        }
    }

    pub fn symlink_metadata(&self, filename: &Utf8Path) -> SftpChannelResult<Metadata> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.lstat(filename.as_std_path()).map(Metadata::from)?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp
                .symlink_metadata(filename.as_str())
                .map(Metadata::from)?),
        }
    }

    pub fn metadata(&self, filename: &Utf8Path) -> SftpChannelResult<Metadata> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.stat(filename.as_std_path()).map(Metadata::from)?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.metadata(filename.as_str()).map(Metadata::from)?),
        }
    }

    pub fn set_metadata(&self, filename: &Utf8Path, metadata: Metadata) -> SftpChannelResult<()> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.setstat(filename.as_std_path(), metadata.into())?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => {
                let attr: libssh_rs::SetAttributes = metadata.into();
                Ok(sftp.set_metadata(filename.as_str(), &attr)?)
            }
        }
    }

    pub fn open_dir(&self, filename: &Utf8Path) -> SftpChannelResult<DirWrap> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => Ok(sftp.opendir(filename.as_std_path()).map(DirWrap::Ssh2)?),

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => Ok(sftp.open_dir(filename.as_str()).map(DirWrap::LibSsh)?),
        }
    }

    pub fn read_dir(&self, filename: &Utf8Path) -> SftpChannelResult<Vec<(Utf8PathBuf, Metadata)>> {
        match self {
            #[cfg(feature = "ssh2")]
            Self::Ssh2(sftp) => {
                let entries = sftp.readdir(filename.as_std_path())?;
                let mut mapped_entries = vec![];
                for (path, stat) in entries {
                    let path = pathconv(path)?;
                    mapped_entries.push((path, Metadata::from(stat)));
                }

                Ok(mapped_entries)
            }

            #[cfg(feature = "libssh-rs")]
            Self::LibSsh(sftp) => {
                let entries = sftp.read_dir(filename.as_str())?;
                let mut mapped_entries = vec![];
                for metadata in entries {
                    let path = metadata
                        .name()
                        .expect("name to be present in read dir results");
                    if path == "." || path == ".." {
                        continue;
                    }
                    mapped_entries.push((filename.join(path), metadata.into()));
                }

                Ok(mapped_entries)
            }
        }
    }
}
