use pyo3::create_exception;
use pyo3::exceptions::{PyException, PyValueError};
use pyo3::import_exception;
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyType};

use std::collections::VecDeque;
use std::io::{Read, Write};
use std::sync::Arc;

/// Synchronous bidirectional byte stream the SFTP client can run over.
/// Either a `std::fs::File` (subprocess vendors hand us an fd) or the
/// russh blocking adapter. Boxed so `SFTPClient` has a single concrete
/// generic instantiation regardless of backend.
pub(crate) trait SshChannel: Read + Write + Send {}
impl<T: Read + Write + Send + ?Sized> SshChannel for T {}

pub(crate) type BoxedChannel = Box<dyn SshChannel>;

create_exception!(dromedary._transport_rs, SFTPError, PyException);
import_exception!(dromedary.errors, NoSuchFile);
import_exception!(dromedary.errors, PermissionDenied);
import_exception!(dromedary.errors, SocketConnectionError);

#[pyclass]
struct SFTPAttributes(sftp::Attributes);

#[pymethods]
impl SFTPAttributes {
    #[new]
    fn new() -> Self {
        Self(sftp::Attributes::new())
    }

    #[getter]
    fn get_st_mode(&self) -> Option<u32> {
        self.0.permissions
    }

    #[setter]
    fn set_st_mode(&mut self, mode: Option<u32>) {
        self.0.permissions = mode;
    }

    #[getter]
    fn get_st_size(&self) -> Option<u64> {
        self.0.size
    }

    #[setter]
    fn set_st_size(&mut self, size: Option<u64>) {
        self.0.size = size;
    }
}

#[pyclass]
pub(crate) struct SFTPClient {
    sftp: Arc<sftp::SftpClient<BoxedChannel>>,
    cwd: Option<String>,
}

impl SFTPClient {
    /// Construct an SFTP client over an arbitrary sync byte-stream channel.
    /// Used by library-backed SSH vendors (russh etc.) that produce a stream
    /// rather than a kernel fd.
    #[cfg(feature = "russh")]
    pub(crate) fn from_channel(channel: BoxedChannel) -> std::io::Result<Self> {
        let session = sftp::SftpClient::new(channel)?;
        Ok(Self {
            sftp: Arc::new(session),
            cwd: None,
        })
    }
}

fn sftp_error_to_py_err(e: sftp::Error, path: Option<&str>) -> PyErr {
    match e {
        sftp::Error::Io(e) => e.into(),
        sftp::Error::Eof(_, _) => std::io::Error::from(std::io::ErrorKind::UnexpectedEof).into(),
        sftp::Error::NoSuchFile(msg, _lang) => {
            NoSuchFile::new_err((path.map(|p| p.to_string()), msg))
        }
        sftp::Error::PermissionDenied(msg, _) => {
            PermissionDenied::new_err((path.map(|p| p.to_string()), msg))
        }
        sftp::Error::Failure(msg, _lang) => SFTPError::new_err(msg),
        _ => SFTPError::new_err(format!("{:?}", e)),
    }
}

#[pyclass]
struct SFTPFile {
    sftp: Arc<sftp::SftpClient<BoxedChannel>>,
    file: sftp::File,
    offset: u64,
}

impl SFTPClient {
    fn _adjust_cwd(&self, path: &str) -> String {
        if self.cwd.is_none() {
            return path.to_string();
        }

        if path.starts_with('/') {
            return path.to_string();
        }

        if self.cwd == Some("/".to_owned()) {
            return format!("/{}", path);
        }

        format!("{}/{}", self.cwd.as_ref().unwrap(), path)
    }
}

#[pymethods]
impl SFTPFile {
    fn block(&mut self, py: Python, offset: u64, length: u64, lockmask: u32) -> PyResult<()> {
        py.detach(|| self.sftp.block(&self.file, offset, length, lockmask))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    fn unblock(&mut self, py: Python, offset: u64, length: u64) -> PyResult<()> {
        py.detach(|| self.sftp.unblock(&self.file, offset, length))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    fn setstat(&mut self, py: Python, attr: &SFTPAttributes) -> PyResult<()> {
        py.detach(|| self.sftp.fsetstat(&self.file, &attr.0))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    #[pyo3(signature = (flags = None))]
    fn stat(&mut self, py: Python, flags: Option<u32>) -> PyResult<SFTPAttributes> {
        py.detach(|| self.sftp.fstat(&self.file, flags).map(SFTPAttributes))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    fn flush(&mut self, _py: Python) -> PyResult<()> {
        Ok(())
    }

    fn pwrite(&mut self, py: Python, offset: u64, data: &[u8]) -> PyResult<()> {
        py.detach(|| self.sftp.pwrite(&self.file, offset, data))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    fn pread(&mut self, py: Python, offset: u64, length: u32) -> PyResult<Py<PyAny>> {
        py.detach(|| self.sftp.pread(&self.file, offset, length))
            .map_err(|e| sftp_error_to_py_err(e, None))
            .map(|b| PyBytes::new(py, &b).into())
    }

    fn close(&mut self, py: Python) -> PyResult<()> {
        py.detach(|| self.sftp.fclose(&self.file))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    fn seekable(&self) -> bool {
        true
    }

    fn tell(&self) -> u64 {
        self.offset
    }

    fn seek(&mut self, py: Python, offset: i64, whence: u32) -> PyResult<u64> {
        let size = self.stat(py, None)?.0.size.unwrap();
        let new_offset = match whence {
            // SEEK_SET
            0 => offset,
            // SEEK_CUR
            1 => self.offset as i64 + offset,
            // SEEK_END
            2 => size as i64 - offset,
            _ => {
                return Err(PyValueError::new_err(("Invalid whence",)));
            }
        };

        if new_offset < 0 {
            return Err(PyValueError::new_err((format!(
                "Negative offset: {}",
                new_offset
            ),)));
        }

        self.offset = new_offset as u64;
        Ok(self.offset)
    }

    fn readv<'a>(
        &mut self,
        py: Python<'a>,
        offsets: Vec<(u64, u32)>,
    ) -> PyResult<Bound<'a, PyAny>> {
        #[pyclass]
        struct ReadvIter {
            offsets: VecDeque<(u64, u32)>,
            sftp: Arc<sftp::SftpClient<BoxedChannel>>,
            file: sftp::File,
        }

        #[pymethods]
        impl ReadvIter {
            fn __iter__(slf: PyRef<Self>) -> Py<Self> {
                slf.into()
            }

            fn __next__(&mut self, py: Python) -> PyResult<Option<Py<PyAny>>> {
                if let Some((offset, length)) = self.offsets.pop_front() {
                    match py.detach(|| self.sftp.pread(&self.file, offset, length)) {
                        Ok(data) => Ok(Some(PyBytes::new(py, &data).into())),
                        Err(sftp::Error::Eof(_, _)) => Ok(Some(PyBytes::new(py, &[]).into())),
                        Err(e) => Err(sftp_error_to_py_err(e, None)),
                    }
                } else {
                    Ok(None)
                }
            }
        }

        Ok(Bound::new(
            py,
            ReadvIter {
                offsets: VecDeque::from(offsets),
                sftp: Arc::clone(&self.sftp),
                file: self.file.clone(),
            },
        )?
        .into_any())
    }

    #[pyo3(signature = (length = None))]
    fn read<'a>(&mut self, py: Python<'a>, length: Option<u32>) -> PyResult<Bound<'a, PyBytes>> {
        let ret = if let Some(length) = length {
            py.detach(|| self.sftp.pread(&self.file, self.offset, length))
        } else {
            let length = self.stat(py, None)?.0.size.unwrap();
            if length == 0 {
                return Ok(PyBytes::new(py, &[]));
            }
            py.detach(|| {
                self.sftp
                    .pread(&self.file, self.offset, (length - self.offset) as u32)
            })
        };
        match ret {
            Ok(data) => {
                self.offset += data.len() as u64;
                Ok(PyBytes::new(py, data.as_slice()))
            }
            Err(sftp::Error::Eof(_, _)) => Ok(PyBytes::new(py, &[])),
            Err(e) => Err(sftp_error_to_py_err(e, None)),
        }
    }

    fn write(&mut self, py: Python, data: &[u8]) -> PyResult<()> {
        py.detach(|| self.sftp.pwrite(&self.file, self.offset, data))
            .map_err(|e| sftp_error_to_py_err(e, None))?;
        self.offset += data.len() as u64;
        Ok(())
    }

    fn __enter__(slf: PyRef<Self>) -> Py<Self> {
        slf.into()
    }

    fn __exit__(
        &mut self,
        py: Python,
        _exc_type: Option<&Bound<PyType>>,
        _exc_val: Option<&Bound<PyAny>>,
        _exc_tb: Option<&Bound<PyAny>>,
    ) -> PyResult<bool> {
        self.close(py)?;
        Ok(false)
    }
}

#[pyclass]
struct SFTPDir(Arc<sftp::SftpClient<BoxedChannel>>, sftp::Directory);

#[pymethods]
impl SFTPDir {
    fn readdir(&mut self, py: Python) -> PyResult<Option<Vec<(String, String, SFTPAttributes)>>> {
        match py.detach(|| {
            self.0.readdir(&self.1).map(|e| {
                e.into_iter()
                    .map(|(k, l, v)| (k, l, SFTPAttributes(v)))
                    .collect::<Vec<_>>()
            })
        }) {
            Ok(v) => Ok(Some(v)),
            Err(sftp::Error::Eof(_, _)) => Ok(None),
            Err(e) => Err(sftp_error_to_py_err(e, None)),
        }
    }

    fn close(&mut self, py: Python) -> PyResult<()> {
        py.detach(|| self.0.closedir(&self.1))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }
}

#[pymethods]
impl SFTPClient {
    #[new]
    fn new(py: Python, fd: isize) -> PyResult<Self> {
        let session = py
            .detach(|| {
                #[cfg(unix)]
                let channel: BoxedChannel = {
                    use std::os::fd::FromRawFd;
                    // SAFETY: `fd` was produced by a vendor that transferred
                    // ownership via `detach_fd`; wrapping it in `File` makes
                    // us the sole owner, closed on `Drop`.
                    Box::new(unsafe { std::fs::File::from_raw_fd(fd as i32) })
                };
                #[cfg(windows)]
                let channel: BoxedChannel = {
                    use std::os::windows::io::{FromRawHandle, RawHandle};
                    // SAFETY: same detach-and-transfer contract as unix above.
                    Box::new(unsafe { std::fs::File::from_raw_handle(fd as RawHandle) })
                };
                sftp::SftpClient::new(channel)
            })
            .map_err(|e| {
                // An IO error during the opening SFTP handshake means the
                // transport (spawned ssh subprocess or plain TCP socket)
                // died before we could speak SFTP to it. Report it as a
                // transport-level connection failure rather than letting a
                // raw BrokenPipeError / EOF surface at the caller.
                SocketConnectionError::new_err((
                    "".to_string(),
                    "".to_string(),
                    "Failed to open SFTP session",
                    e.to_string(),
                ))
            })?;
        Ok(Self {
            sftp: Arc::new(session),
            cwd: None,
        })
    }

    #[pyo3(signature = (path, mode = None))]
    fn mkdir(&mut self, py: Python, path: &str, mode: Option<u32>) -> PyResult<()> {
        let path = self._adjust_cwd(path);
        let mut attr = sftp::Attributes::new();
        attr.permissions = Some(mode.unwrap_or(0o777) | 0o40000);
        py.detach(|| self.sftp.mkdir(path.as_str(), &attr))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    fn extended(&mut self, py: Python, extension: &str, data: &[u8]) -> PyResult<Option<Vec<u8>>> {
        py.detach(|| self.sftp.extended(extension, data))
            .map_err(|e| sftp_error_to_py_err(e, None))
    }

    #[pyo3(signature = (path, flags = None))]
    fn lstat(&mut self, py: Python, path: &str, flags: Option<u32>) -> PyResult<SFTPAttributes> {
        let path = self._adjust_cwd(path);
        py.detach(|| self.sftp.lstat(path.as_str(), flags))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
            .map(SFTPAttributes)
    }

    #[pyo3(signature = (path, flags = None))]
    fn stat(&mut self, py: Python, path: &str, flags: Option<u32>) -> PyResult<SFTPAttributes> {
        let path = self._adjust_cwd(path);
        py.detach(|| self.sftp.stat(path.as_str(), flags))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
            .map(SFTPAttributes)
    }

    fn chmod(&mut self, py: Python, path: &str, mode: u32) -> PyResult<()> {
        let path = self._adjust_cwd(path);
        let attr = sftp::Attributes {
            permissions: Some(mode),
            ..Default::default()
        };
        py.detach(|| self.sftp.setstat(path.as_str(), &attr))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    fn setstat(&mut self, py: Python, path: &str, attr: &SFTPAttributes) -> PyResult<()> {
        let path = self._adjust_cwd(path);
        py.detach(|| self.sftp.setstat(path.as_str(), &attr.0))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    fn hardlink(&mut self, py: Python, oldpath: &str, newpath: &str) -> PyResult<()> {
        let newpath = self._adjust_cwd(newpath);
        py.detach(|| self.sftp.hardlink(oldpath, newpath.as_str()))
            .map_err(|e| sftp_error_to_py_err(e, Some(newpath.as_str())))
    }

    #[pyo3(signature = (path, control_byte = None, compose_path = None))]
    fn realpath(
        &mut self,
        py: Python,
        path: &str,
        control_byte: Option<u8>,
        compose_path: Option<&str>,
    ) -> PyResult<String> {
        let path = self._adjust_cwd(path);
        py.detach(|| {
            self.sftp
                .realpath(path.as_str(), control_byte, compose_path)
        })
        .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    fn symlink(&mut self, py: Python, oldpath: &str, newpath: &str) -> PyResult<()> {
        let newpath = self._adjust_cwd(newpath);
        py.detach(|| self.sftp.symlink(oldpath, newpath.as_str()))
            .map_err(|e| sftp_error_to_py_err(e, Some(newpath.as_str())))
    }

    fn readlink(&mut self, py: Python, path: &str) -> PyResult<String> {
        let path = self._adjust_cwd(path);
        py.detach(|| self.sftp.readlink(path.as_str()))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    #[pyo3(signature = (oldpath, newpath, flags = None))]
    fn rename(
        &mut self,
        py: Python,
        oldpath: &str,
        newpath: &str,
        flags: Option<u32>,
    ) -> PyResult<()> {
        let newpath = self._adjust_cwd(newpath);
        let oldpath = self._adjust_cwd(oldpath);
        py.detach(|| self.sftp.rename(oldpath.as_str(), newpath.as_str(), flags))
            .map_err(|e| sftp_error_to_py_err(e, Some(newpath.as_str())))
    }

    fn remove(&mut self, py: Python, path: &str) -> PyResult<()> {
        let path = self._adjust_cwd(path);
        py.detach(|| self.sftp.remove(path.as_str()))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    fn rmdir(&mut self, py: Python, path: &str) -> PyResult<()> {
        let path = self._adjust_cwd(path);
        py.detach(|| self.sftp.rmdir(path.as_str()))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))
    }

    fn close(&mut self) -> PyResult<()> {
        Ok(())
    }

    #[pyo3(signature = (path, attr, *, read=false, write=false, append=false, create=false, truncate=false, excl=false))]
    #[allow(clippy::too_many_arguments)]
    fn open(
        &mut self,
        py: Python,
        path: &str,
        attr: &SFTPAttributes,
        read: bool,
        write: bool,
        append: bool,
        create: bool,
        truncate: bool,
        excl: bool,
    ) -> PyResult<SFTPFile> {
        let path = self._adjust_cwd(path);
        let options = sftp::OpenOptions::new()
            .read(read)
            .write(write)
            .append(append)
            .create(create)
            .truncate(truncate)
            .excl(excl);
        let h = py
            .detach(|| self.sftp.open(path.as_str(), options, &attr.0))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))?;
        Ok(SFTPFile {
            sftp: Arc::clone(&self.sftp),
            file: h,
            offset: 0,
        })
    }

    #[pyo3(signature = (path, mode = None, create_mode = None))]
    fn file(
        &mut self,
        py: Python,
        path: &str,
        mode: Option<&str>,
        create_mode: Option<u32>,
    ) -> PyResult<SFTPFile> {
        let path = self._adjust_cwd(path);
        let offset = 0;
        let mode = mode.unwrap_or("rt");
        let options = match mode {
            "rt" | "rb" => sftp::OpenOptions::new().read(true),
            "ab" => sftp::OpenOptions::new()
                .write(true)
                .create(true)
                .append(true),
            "wb" => sftp::OpenOptions::new()
                .read(true)
                .write(true)
                .create(true)
                .truncate(true),
            "r+" | "rb+" | "b+" => sftp::OpenOptions::new().read(true).write(true).create(true),
            "a+" | "ab+" => sftp::OpenOptions::new()
                .read(true)
                .write(true)
                .create(true)
                .append(true),
            mode => panic!("Unsupported mode: {}", mode),
        };

        let attr = sftp::Attributes {
            permissions: create_mode,
            ..Default::default()
        };

        let h = py
            .detach(|| self.sftp.open(path.as_str(), options, &attr))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))?;

        let mut ret = SFTPFile {
            sftp: Arc::clone(&self.sftp),
            file: h,
            offset,
        };

        if mode.contains('a') {
            ret.seek(py, 0, 2)?;
        }

        Ok(ret)
    }

    fn opendir(&mut self, py: Python, path: &str) -> PyResult<SFTPDir> {
        let path = self._adjust_cwd(path);
        let h = py
            .detach(|| self.sftp.opendir(path.as_str()))
            .map_err(|e| sftp_error_to_py_err(e, Some(path.as_str())))?;
        Ok(SFTPDir(Arc::clone(&self.sftp), h))
    }

    fn listdir(&mut self, py: Python, path: &str) -> PyResult<Vec<String>> {
        let path = self._adjust_cwd(path);
        let mut dir = self.opendir(py, path.as_str())?;
        let mut entries = Vec::new();
        while let Some(extra_entries) = dir.readdir(py)? {
            for (name, _, _) in extra_entries {
                entries.push(name);
            }
        }
        Ok(entries)
    }
}

pub fn _sftp_rs(py: Python, m: &Bound<PyModule>) -> PyResult<()> {
    m.add_class::<SFTPClient>()?;
    m.add_class::<SFTPAttributes>()?;
    m.add("SFTPError", py.get_type::<SFTPError>())?;
    Ok(())
}
