use ::log::debug;
use dromedary::lock::{FileLock, Lock as LockTrait, LockError};
use dromedary::{Error, ReadStream, Transport as TransportTrait, UrlFragment, WriteStream};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::import_exception;
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyIterator, PyList, PyType};
use pyo3_filelike::PyBinaryFile;
use std::collections::HashMap;
use std::fs::Permissions;
use std::io::{BufRead, BufReader, Read, Seek, Write};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use url::Url;

import_exception!(dromedary.errors, TransportError);
import_exception!(dromedary.errors, NotLocalUrl);
import_exception!(dromedary.errors, InProcessTransport);
import_exception!(dromedary.errors, NoSuchFile);
import_exception!(dromedary.errors, FileExists);
import_exception!(dromedary.errors, PathNotChild);
import_exception!(dromedary.errors, PermissionDenied);
import_exception!(dromedary.errors, TransportNotPossible);
import_exception!(dromedary.errors, ShortReadvError);
import_exception!(dromedary.errors, LockContention);
import_exception!(dromedary.errors, LockFailed);
import_exception!(dromedary.errors, ReadError);
import_exception!(dromedary.errors, PathError);
import_exception!(dromedary.errors, DirectoryNotEmpty);
import_exception!(dromedary.errors, NotADirectory);
import_exception!(dromedary.errors, ResourceBusy);
import_exception!(dromedary.errors, InvalidHttpResponse);
import_exception!(dromedary.errors, UnexpectedHttpStatus);
import_exception!(dromedary.errors, InvalidHttpRange);
import_exception!(dromedary.errors, BadHttpRequest);
import_exception!(dromedary.errors, RedirectRequested);
import_exception!(dromedary.errors, UnusableRedirect);
import_exception!(dromedary.errors, ConnectionError);
import_exception!(dromedary.urlutils, InvalidURL);

#[pyclass(subclass)]
pub(crate) struct Transport(pub(crate) Box<dyn TransportTrait>);

/// Python-visible iterator wrapping the Rust-side `readv` iterator.
///
/// Holds a strong reference to the parent Transport pyclass
/// (`Py<Transport>`) so the `'static`-transmuted inner iterator
/// never outlives the transport it borrows from. Each `__next__`
/// pulls one element from the Rust iterator under `py.detach` so
/// HTTP calls in the iterator body can block without holding the
/// GIL — the in-process Python test HTTP server lives in another
/// thread and needs the GIL to serve requests.
#[pyclass]
pub(crate) struct ReadvIter {
    /// Keeps the underlying Transport (and therefore the `dyn
    /// TransportTrait` + client state the iterator closed over)
    /// alive for as long as the iterator is live. Python holds a
    /// strong reference via this `Py`.
    #[allow(dead_code)]
    parent: Py<Transport>,
    /// The Rust iterator. `Option` so `__next__` can hand out the
    /// final `None` cleanly and drop the iterator early if a
    /// caller leaks the iterator pyclass. `Mutex` because PyO3's
    /// `#[pymethods]` takes `&self`, but iterator advancement
    /// needs mutation.
    iter: Mutex<Option<Box<dyn Iterator<Item = Result<(u64, Vec<u8>), dromedary::Error>> + Send>>>,
    /// Path string used for error-mapping; carried alongside the
    /// iterator for easy access on `__next__`'s Err path.
    path: String,
}

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

    fn __next__<'py>(&self, py: Python<'py>) -> PyResult<Option<(u64, Bound<'py, PyBytes>)>> {
        let result = py.detach(|| {
            let mut guard = self.iter.lock().unwrap();
            guard.as_mut().and_then(|it| it.next())
        });
        match result {
            None => {
                // Drop the iterator so cached HTTP state releases
                // as soon as the caller stops consuming.
                *self.iter.lock().unwrap() = None;
                Ok(None)
            }
            Some(Err(e)) => {
                *self.iter.lock().unwrap() = None;
                Err(map_transport_err_to_py_err(e, None, Some(&self.path)))
            }
            Some(Ok((offset, data))) => Ok(Some((offset, PyBytes::new(py, &data)))),
        }
    }
}

/// Python-visible base class for Rust-backed transports that talk to
/// a remote server. Mirrors `dromedary.ConnectedTransport` on the
/// Python side, providing the `_user` / `_password` / `_host` /
/// `_port` / `_path` / `_scheme` getters and `disconnect()` /
/// `_reuse_for()` that `get_transport_from_url(possible_transports=…)`
/// relies on for connection pooling.
#[pyclass(extends=Transport, subclass)]
pub(crate) struct ConnectedTransport;

#[pymethods]
impl ConnectedTransport {
    #[getter]
    fn _scheme(slf: PyRef<'_, Self>) -> String {
        let base = slf.as_super().0.base();
        dromedary::connected_url_scheme(&base)
    }

    #[getter]
    fn _host(slf: PyRef<'_, Self>) -> Option<String> {
        let base = slf.as_super().0.base();
        dromedary::connected_url_host(&base)
    }

    #[getter]
    fn _port(slf: PyRef<'_, Self>) -> Option<u16> {
        let base = slf.as_super().0.base();
        dromedary::connected_url_port(&base)
    }

    #[getter]
    fn _user(slf: PyRef<'_, Self>) -> Option<String> {
        let base = slf.as_super().0.base();
        dromedary::connected_url_user(&base)
    }

    #[getter]
    fn _password(slf: PyRef<'_, Self>) -> Option<String> {
        let base = slf.as_super().0.base();
        dromedary::connected_url_password(&base)
    }

    #[getter]
    fn _path(slf: PyRef<'_, Self>) -> String {
        let base = slf.as_super().0.base();
        dromedary::connected_url_path(&base)
    }

    /// Parsed form of the base URL as a `dromedary.urlutils.URL`.
    /// Historically set on the Python `ConnectedTransport` at
    /// construction; breezy's `transport/remote.py` reads it for
    /// host / port / user / password access. We build one on
    /// demand so Rust-backed transports don't have to carry extra
    /// state.
    #[getter]
    fn _parsed_url<'py>(slf: PyRef<'py, Self>, py: Python<'py>) -> PyResult<Py<PyAny>> {
        let base = slf.as_super().0.base();
        let base_str = base.to_string();
        let module = py.import("dromedary.urlutils")?;
        let cls = module.getattr("URL")?;
        let url = cls.call_method1("from_string", (base_str.as_str(),))?;
        Ok(url.unbind())
    }

    /// Shared-connection handle. Breezy compares
    /// `_get_connection()` results across clones to decide whether
    /// two transports share an underlying connection. We can't
    /// expose the Rust `Arc<HttpClient>` identity directly across
    /// the FFI boundary, so we use the next best signal: the
    /// `(scheme, host, port)` tuple. Two transports with the same
    /// origin share the same `HttpClient` pool entry and from
    /// breezy's perspective are "the same connection"; transports
    /// at different paths under the same origin (e.g. after a
    /// redirect from `/foo/` to `/foo/subdir/`) are also "the same
    /// connection" and should compare equal here.
    fn _get_connection<'py>(slf: PyRef<'py, Self>, py: Python<'py>) -> PyResult<Py<PyAny>> {
        let base = slf.as_super().0.base();
        let key = format!(
            "{}://{}:{}",
            base.scheme(),
            base.host_str().unwrap_or(""),
            base.port_or_known_default().unwrap_or(0),
        );
        Ok(pyo3::types::PyString::new(py, &key).into())
    }

    /// Default `disconnect` — a no-op. Concrete transports with an
    /// explicit connection handle override this in Python (or extend
    /// the pyclass and override the pymethod).
    fn disconnect(&self) {}

    /// Return a transport for `other_base` sharing this transport's
    /// connection state, or `None` if the URLs point at different
    /// origins.
    ///
    /// The comparison logic lives in pure Rust (`classify_reuse_for`)
    /// so pure-Rust callers use the same rules. This layer just
    /// dispatches on the result: same-origin-same-path returns
    /// `self`, same-origin-different-path calls back into Python to
    /// let subclass `__init__` flow through with its extra kwargs
    /// (e.g. HTTPS_transport's `ca_certs`).
    fn _reuse_for<'py>(
        slf: PyRef<'py, Self>,
        py: Python<'py>,
        other_base: &str,
    ) -> PyResult<Py<PyAny>> {
        let base = slf.as_super().0.base();
        match dromedary::classify_reuse_for(&base, other_base) {
            dromedary::ReuseMatch::None => Ok(py.None()),
            dromedary::ReuseMatch::Same => Ok(slf.into_pyobject(py)?.into_any().unbind()),
            dromedary::ReuseMatch::Sibling => {
                let bound = slf.into_pyobject(py)?;
                let cls = bound.get_type();
                let kwargs = pyo3::types::PyDict::new(py);
                kwargs.set_item("_from_transport", bound.clone())?;
                let sibling = cls.call((other_base,), Some(&kwargs))?;
                Ok(sibling.unbind())
            }
        }
    }
}

/// Shared base class for all Rust-backed Transport decorators.
///
/// Stores a reference to the wrapped Python transport and provides the
/// common decorator machinery (`_decorated` accessor, `__getattr__`
/// fallback, redirect re-wrapping). Concrete decorators extend this class
/// and only need to supply their URL prefix and their `__new__` / `clone`
/// (which construct the decorator-specific wrapped Rust transport).
#[pyclass(extends=Transport, subclass)]
pub(crate) struct TransportDecorator {
    pub(crate) decorated: Py<PyAny>,
    pub(crate) prefix: &'static str,
}

#[pymethods]
impl TransportDecorator {
    #[getter]
    fn _decorated(&self, py: Python) -> Py<PyAny> {
        self.decorated.clone_ref(py)
    }

    #[classmethod]
    fn _get_url_prefix(_cls: &Bound<'_, pyo3::types::PyType>) -> PyResult<&'static str> {
        // Subclasses override this to return their static prefix. Called on
        // the class itself, not the instance, so we can't consult `prefix`
        // here — but since this is the abstract base, we shouldn't be called
        // on it directly. Return empty to make the failure obvious.
        Ok("")
    }

    /// Forward unrecognised attribute access to the wrapped transport.
    ///
    /// Decorators mix Rust (base Transport methods) with Python (extras like
    /// HttpTransport.request). Anything not bound on the decorator itself
    /// should pass through to the inner transport.
    fn __getattr__(&self, py: Python, name: &str) -> PyResult<Py<PyAny>> {
        self.decorated.bind(py).getattr(name).map(|b| b.unbind())
    }

    /// Re-wrap the redirected transport so the decorator qualifier is
    /// preserved across HTTP redirects.
    fn _redirected_to<'a>(
        slf: PyRef<'a, Self>,
        py: Python<'a>,
        source: &str,
        target: &str,
    ) -> PyResult<Option<Py<PyAny>>> {
        let inner = slf.decorated.bind(py);
        let redirected = inner.call_method1("_redirected_to", (source, target))?;
        if redirected.is_none() {
            return Ok(None);
        }
        let base: String = redirected.getattr("base")?.extract()?;
        let new_url = format!("{}{}", slf.prefix, base);
        let cls = slf.into_pyobject(py)?.get_type();
        let instance = cls.call1((new_url, redirected.clone().unbind()))?;
        Ok(Some(instance.unbind()))
    }
}

pub(crate) fn map_transport_err_to_py_err(
    e: Error,
    t: Option<Py<PyAny>>,
    p: Option<&UrlFragment>,
) -> PyErr {
    let pick_path = |n: Option<String>| n.or_else(|| p.map(|p| p.to_string()));
    match e {
        Error::InProcessTransport => InProcessTransport::new_err((t,)),
        Error::NotLocalUrl(url) => NotLocalUrl::new_err((url,)),
        Error::NoSuchFile(name) => NoSuchFile::new_err((pick_path(name),)),
        Error::FileExists(name) => FileExists::new_err((pick_path(name),)),
        Error::TransportNotPossible(msg) => TransportNotPossible::new_err((msg,)),
        Error::UrlError(_e) => InvalidURL::new_err((p.map(|p| p.to_string()),)),
        Error::PermissionDenied(name) => PermissionDenied::new_err((pick_path(name),)),
        Error::PathNotChild => {
            PathNotChild::new_err((p.map(|p| p.to_string()).unwrap_or_default(), "".to_string()))
        }
        Error::UrlutilsError(_e) => InvalidURL::new_err((p.map(|p| p.to_string()),)),
        Error::Io(e) => e.into(),
        Error::UnexpectedEof => PyValueError::new_err("Unexpected EOF"),
        Error::LockContention(name) => LockContention::new_err((name,)),
        Error::LockFailed(name, error) => LockFailed::new_err((name, error)),
        Error::NotADirectoryError(name) => NoSuchFile::new_err((pick_path(name),)),
        Error::IsADirectoryError(name) => ReadError::new_err((pick_path(name), "is a directory")),
        Error::DirectoryNotEmptyError(name) => DirectoryNotEmpty::new_err((pick_path(name),)),
        Error::ShortReadvError(path, offset, expected, got) => {
            ShortReadvError::new_err((path, offset, expected, got))
        }
        Error::ResourceBusy(name) => ResourceBusy::new_err((pick_path(name),)),
        Error::InvalidHttpResponse { path, msg } => InvalidHttpResponse::new_err((path, msg)),
        Error::UnexpectedHttpStatus { path, code, extra } => {
            UnexpectedHttpStatus::new_err((path, code, extra))
        }
        Error::InvalidHttpRange { path, range, msg } => {
            InvalidHttpRange::new_err((path, range, msg))
        }
        Error::BadHttpRequest { path, reason } => BadHttpRequest::new_err((path, reason)),
        Error::RedirectRequested {
            source,
            target,
            is_permanent,
        } => RedirectRequested::new_err((source, target, is_permanent)),
        Error::UnusableRedirect {
            source,
            target,
            reason,
        } => UnusableRedirect::new_err((source, target, reason)),
        Error::ConnectionError(msg) => ConnectionError::new_err((msg,)),
    }
}

/// Convert a Python mode int to Rust `Option<Permissions>`.
///
/// On Unix we translate the int into `Permissions` via `PermissionsExt`.
/// On Windows there is no meaningful mapping — Python's own transport layer
/// ignores the mode argument there — so we discard it and return `None`.
#[cfg(unix)]
fn perms_from_py_object(obj: Py<PyAny>) -> Option<Permissions> {
    Python::attach(|py| {
        let mode = obj.extract::<u32>(py).ok()?;
        Some(Permissions::from_mode(mode))
    })
}

#[cfg(not(unix))]
fn perms_from_py_object(_obj: Py<PyAny>) -> Option<Permissions> {
    None
}

/// Default permissions to apply when Python passes no mode.
/// On Unix this mirrors the process umask; on Windows we have nothing
/// meaningful to set, so we return `None`.
#[cfg(unix)]
fn default_perms() -> Option<Permissions> {
    use nix::sys::stat::{umask, Mode};
    let mask = umask(Mode::empty());
    umask(mask);
    let mode = 0o666 & !mask.bits();
    Some(Permissions::from_mode(mode as u32))
}

#[cfg(not(unix))]
fn default_perms() -> Option<Permissions> {
    None
}

#[pyclass]
struct PyStat {
    #[pyo3(get)]
    st_mode: u32,

    #[pyo3(get)]
    st_size: usize,

    #[pyo3(get)]
    st_mtime: Option<f64>,
}

trait BufReadStream: BufRead + Seek {}

impl BufReadStream for BufReader<Box<dyn ReadStream + Sync + Send>> {}

#[pyclass]
struct PyBufReadStream {
    f: Box<dyn BufReadStream + Sync + Send>,
    path: PathBuf,
}

#[pyclass]
struct PyWriteStream(Option<Box<dyn WriteStream + Sync + Send>>);

impl PyWriteStream {
    fn inner_mut(&mut self) -> PyResult<&mut (dyn WriteStream + Sync + Send)> {
        match self.0.as_deref_mut() {
            Some(s) => Ok(s),
            None => Err(pyo3::exceptions::PyValueError::new_err(
                "I/O operation on closed file",
            )),
        }
    }
}

#[pymethods]
impl PyWriteStream {
    fn write(&mut self, py: Python, data: &[u8]) -> PyResult<usize> {
        let inner = self.inner_mut()?;
        py.detach(|| inner.write(data)).map_err(|e| e.into())
    }

    #[pyo3(signature = (want_fdatasync=None))]
    fn close(&mut self, py: Python, want_fdatasync: Option<bool>) -> PyResult<()> {
        if want_fdatasync.unwrap_or(false) {
            if let Err(err) = self.fdatasync(py) {
                if !err.is_instance_of::<TransportNotPossible>(py) {
                    return Err(err);
                }
            }
        }
        // Drop the underlying writer so its file descriptor is released
        // immediately — Python's FileStream.close() contract promises
        // the file is no longer in use after this returns.
        self.0 = None;
        Ok(())
    }

    fn fdatasync(&mut self, py: Python) -> PyResult<()> {
        let inner = self.inner_mut()?;
        py.detach(|| inner.sync_data()).map_err(|e| e.into())
    }

    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, None)?;
        Ok(false)
    }

    fn flush(&mut self, py: Python) -> PyResult<()> {
        let inner = self.inner_mut()?;
        py.detach(|| inner.flush()).map_err(|e| e.into())
    }

    fn writelines(&mut self, py: Python, lines: &Bound<PyList>) -> PyResult<()> {
        for line in lines.iter() {
            self.write(py, line.extract::<&[u8]>().unwrap())?;
        }
        Ok(())
    }
}

impl PyBufReadStream {
    fn new(read: Box<dyn ReadStream + Sync + Send>, path: &Path) -> Self {
        Self {
            f: Box::new(BufReader::new(read)),
            path: path.to_path_buf(),
        }
    }

    fn map_io_err_to_py_err(&self, e: std::io::Error) -> PyErr {
        let transport_err =
            dromedary::map_io_err_to_transport_err(e, Some(&self.path.as_path().to_string_lossy()));
        map_transport_err_to_py_err(
            transport_err,
            None,
            Some(self.path.as_path().to_string_lossy().as_ref()),
        )
    }
}

#[pymethods]
impl PyBufReadStream {
    #[pyo3(signature = (size=None))]
    fn read<'a>(&mut self, py: Python<'a>, size: Option<usize>) -> PyResult<Bound<'a, PyBytes>> {
        if let Some(size) = size {
            let mut buf = vec![0; size];
            let ret = py
                .detach(|| self.f.read(&mut buf))
                .map_err(|e| self.map_io_err_to_py_err(e))?;
            Ok(PyBytes::new(py, &buf[..ret]))
        } else {
            let mut buf = Vec::new();
            py.detach(|| self.f.read_to_end(&mut buf))
                .map_err(|e| self.map_io_err_to_py_err(e))?;
            Ok(PyBytes::new(py, &buf))
        }
    }

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

    #[pyo3(signature = (offset, whence=None))]
    fn seek(&mut self, py: Python, offset: i64, whence: Option<i8>) -> PyResult<u64> {
        let seekfrom = match whence.unwrap_or(0) {
            0 => std::io::SeekFrom::Start(offset as u64),
            1 => std::io::SeekFrom::Current(offset),
            2 => std::io::SeekFrom::End(offset),
            _ => return Err(PyValueError::new_err("Invalid whence")),
        };

        py.detach(|| self.f.seek(seekfrom))
            .map_err(|e| self.map_io_err_to_py_err(e))
    }

    fn tell(&mut self, py: Python) -> PyResult<u64> {
        py.detach(|| self.f.stream_position())
            .map_err(|e| self.map_io_err_to_py_err(e))
    }

    fn readline<'a>(&mut self, py: Python<'a>) -> PyResult<Bound<'a, PyBytes>> {
        let mut buf = vec![];
        let ret = py
            .detach(|| self.f.read_until(b'\n', &mut buf))
            .map_err(|e| self.map_io_err_to_py_err(e))?;
        buf.truncate(ret);
        Ok(PyBytes::new(py, &buf))
    }

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

    fn __next__<'a>(&mut self, py: Python<'a>) -> PyResult<Option<Bound<'a, PyBytes>>> {
        let mut buf = vec![];
        let ret = py
            .detach(|| self.f.read_until(b'\n', &mut buf))
            .map_err(|e| self.map_io_err_to_py_err(e))?;
        if ret == 0 {
            return Ok(None);
        }
        buf.truncate(ret);
        Ok(Some(PyBytes::new(py, &buf)))
    }

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

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

    fn __exit__(
        &self,
        _exc_type: Option<&Bound<PyType>>,
        _exc_val: Option<&Bound<PyAny>>,
        _exc_tb: Option<&Bound<PyAny>>,
    ) -> PyResult<bool> {
        Ok(false)
    }

    fn readlines<'a>(&mut self, py: Python<'a>) -> PyResult<Bound<'a, PyList>> {
        let ret = PyList::empty(py);
        while let Some(line) = self.__next__(py)? {
            ret.append(line)?;
        }
        Ok(ret)
    }
}

impl Transport {
    fn map_to_py_err(slf: PyRef<Self>, py: Python, e: Error, p: Option<&str>) -> PyErr {
        let obj = slf.into_pyobject(py).unwrap();
        map_transport_err_to_py_err(e, Some(obj.into()), p)
    }
}

#[pymethods]
impl Transport {
    fn external_url(slf: PyRef<Self>, py: Python) -> PyResult<String> {
        match slf.0.external_url() {
            Ok(url) => Ok(url.to_string()),
            Err(e) => Err(Self::map_to_py_err(slf, py, e, None)),
        }
    }

    fn __repr__(&self) -> PyResult<String> {
        Ok(format!("{:?}", self.0))
    }

    /// Coalesce a list of `(start, length)` offsets into the
    /// fewest number of reads that still covers every byte of the
    /// originals, returning `_CoalescedOffset`-shaped tuples.
    ///
    /// Historically lived on the Python `Transport` base class. Breezy
    /// reaches into it from `transport/remote.py` and from a few
    /// HTTP tests, so we expose it as a staticmethod on the Rust
    /// base pyclass too. Keep the signature (name and arg order)
    /// bit-compatible with the Python original — callers pass 0 for
    /// "no limit" rather than None.
    #[staticmethod]
    #[pyo3(signature = (offsets, limit=None, fudge_factor=None, max_size=None))]
    fn _coalesce_offsets(
        py: Python,
        offsets: Vec<(usize, usize)>,
        limit: Option<usize>,
        fudge_factor: Option<usize>,
        max_size: Option<usize>,
    ) -> PyResult<Py<PyAny>> {
        let raw = coalesce_offsets(offsets, limit, fudge_factor, max_size)?;
        // The Python version returns `_CoalescedOffset` namedtuples
        // (defined in `dromedary.__init__`); wrap each raw tuple so
        // callers that use attribute access (`coal.start`,
        // `coal.length`, `coal.ranges`) keep working.
        let module = py.import("dromedary")?;
        let cls = module.getattr("_CoalescedOffset")?;
        let out = pyo3::types::PyList::empty(py);
        for (start, length, ranges) in raw {
            let tuple = cls.call1((start, length, ranges))?;
            out.append(tuple)?;
        }
        Ok(out.into())
    }

    fn get_bytes<'a>(
        slf: Bound<'a, Self>,
        py: Python<'a>,
        path: &'a str,
    ) -> PyResult<Bound<'a, PyBytes>> {
        let t = &slf.borrow().0;
        let ret = py.detach(|| t.get_bytes(path)).map_err(|e| match e {
            Error::IsADirectoryError(_) => {
                ReadError::new_err((path.to_string(), "Is a directory".to_string()))
            }
            Error::NotADirectoryError(_) => {
                NoSuchFile::new_err((path.to_string(), "Not a directory".to_string()))
            }
            e => {
                let obj = slf.unbind().into_any();
                map_transport_err_to_py_err(e, Some(obj), Some(path))
            }
        })?;

        Ok(PyBytes::new(py, &ret))
    }

    #[getter]
    fn base(&self) -> PyResult<String> {
        Ok(self.0.base().to_string())
    }

    fn has(&self, py: Python, path: &str) -> PyResult<bool> {
        py.detach(|| self.0.has(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
    }

    fn has_any(&self, py: Python, paths: Vec<String>) -> PyResult<bool> {
        let paths = paths.iter().map(|p| p.as_str()).collect::<Vec<_>>();
        py.detach(|| self.0.has_any(paths.as_slice()))
            .map_err(|e| map_transport_err_to_py_err(e, None, None))
    }

    #[pyo3(signature = (path, mode=None))]
    fn mkdir(slf: &Bound<Self>, py: Python, path: &str, mode: Option<Py<PyAny>>) -> PyResult<()> {
        let mode = mode.and_then(perms_from_py_object);
        let t = &slf.borrow().0;
        py.detach(|| t.mkdir(path, mode)).map_err(|e| {
            let obj = slf.clone().unbind().into();
            map_transport_err_to_py_err(e, Some(obj), Some(path))
        })?;
        Ok(())
    }

    #[pyo3(signature = (mode=None))]
    fn ensure_base(&self, py: Python, mode: Option<Py<PyAny>>) -> PyResult<bool> {
        let mode = mode.and_then(perms_from_py_object);
        py.detach(|| self.0.ensure_base(mode))
            .map_err(|e| map_transport_err_to_py_err(e, None, None))
    }

    fn local_abspath(&self, py: Python, path: &str) -> PyResult<String> {
        let path = py
            .detach(|| self.0.local_abspath(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))?;

        path.to_str()
            .map(|s| s.to_string())
            .ok_or_else(|| PyValueError::new_err(format!("Invalid path: {}", path.display())))
    }

    fn get<'a>(
        slf: PyRef<'a, Self>,
        py: Python<'a>,
        path: &'a str,
    ) -> PyResult<Bound<'a, PyBufReadStream>> {
        let t = &slf.0;
        let ret = py.detach(|| t.get(path)).map_err(|e| match e {
            Error::IsADirectoryError(_) => {
                ReadError::new_err((path.to_string(), "Is a directory".to_string()))
            }
            Error::NotADirectoryError(_) => {
                NoSuchFile::new_err((path.to_string(), "Not a directory".to_string()))
            }
            e => {
                let obj = slf.into_pyobject(py).unwrap().unbind().into_any();
                map_transport_err_to_py_err(e, Some(obj), Some(path))
            }
        })?;
        Bound::new(py, PyBufReadStream::new(ret, Path::new(path)))
    }

    fn stat<'a>(&self, py: Python<'a>, path: &str) -> PyResult<Bound<'a, PyStat>> {
        let t = &self.0;
        let stat = py
            .detach(|| t.stat(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))?;
        // On Unix we report the real mode bits; on Windows the `Stat` struct
        // carries no mode (see `project_windows_port.md`) so we synthesise one
        // from the file kind, mirroring what Python's own `os.stat_result`
        // exposes on Windows.
        #[cfg(unix)]
        let st_mode = stat.mode;
        #[cfg(not(unix))]
        let st_mode = {
            use dromedary::FileKind;
            const S_IFDIR: u32 = 0o040000;
            const S_IFREG: u32 = 0o100000;
            const S_IFLNK: u32 = 0o120000;
            let kind_bits = match stat.kind {
                FileKind::Dir => S_IFDIR,
                FileKind::File => S_IFREG,
                FileKind::Symlink => S_IFLNK,
                FileKind::Other => 0,
            };
            kind_bits | 0o777
        };
        Bound::new(
            py,
            PyStat {
                st_size: stat.size,
                st_mode,
                st_mtime: stat.mtime,
            },
        )
    }

    #[pyo3(signature = (path=None))]
    fn relpath(&self, path: Option<&str>) -> PyResult<String> {
        let path = path.unwrap_or(".");
        let url = Url::parse(path).map_err(|_| PyValueError::new_err((path.to_string(),)))?;
        self.0
            .relpath(&url)
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
    }

    fn abspath(&self, path: &str) -> PyResult<String> {
        Ok(self
            .0
            .abspath(path)
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))?
            .to_string())
    }

    #[pyo3(signature = (path, data, mode=None))]
    fn put_bytes(
        slf: &Bound<Self>,
        py: Python,
        path: &str,
        data: &[u8],
        mode: Option<Py<PyAny>>,
    ) -> PyResult<()> {
        let mode = mode.and_then(perms_from_py_object).or_else(default_perms);
        let t = &slf.borrow().0;
        py.detach(|| t.put_bytes(path, data, mode)).map_err(|e| {
            let obj = slf.clone().unbind().into();
            map_transport_err_to_py_err(e, Some(obj), Some(path))
        })?;
        Ok(())
    }

    #[pyo3(signature = (path, data, mode=None, create_parent_dir=None, dir_mode=None))]
    fn put_bytes_non_atomic(
        slf: &Bound<Self>,
        py: Python,
        path: &str,
        data: &[u8],
        mode: Option<Py<PyAny>>,
        create_parent_dir: Option<bool>,
        dir_mode: Option<Py<PyAny>>,
    ) -> PyResult<()> {
        let t = &slf.borrow().0;

        py.detach(|| {
            t.put_bytes_non_atomic(
                path,
                data,
                mode.and_then(perms_from_py_object).or_else(default_perms),
                create_parent_dir,
                dir_mode.and_then(perms_from_py_object),
            )
        })
        .map_err(|e| {
            let obj = slf.clone().unbind().into();
            map_transport_err_to_py_err(e, Some(obj), Some(path))
        })?;
        Ok(())
    }

    #[pyo3(signature = (path, file, mode=None))]
    fn put_file(
        slf: &Bound<Self>,
        py: Python,
        path: &str,
        file: Py<PyAny>,
        mode: Option<Py<PyAny>>,
    ) -> PyResult<u64> {
        let t = &slf.borrow().0;
        let mut file = PyBinaryFile::from(file);
        let ret = py
            .detach(|| {
                t.put_file(
                    path,
                    &mut file,
                    mode.and_then(perms_from_py_object).or_else(default_perms),
                )
            })
            .map_err(|e| {
                let obj = slf.clone().unbind().into();
                map_transport_err_to_py_err(e, Some(obj), Some(path))
            })?;
        Ok(ret)
    }

    #[pyo3(signature = (path, file, mode=None, create_parent_dir=None, dir_mode=None))]
    fn put_file_non_atomic(
        slf: &Bound<Self>,
        py: Python,
        path: &str,
        file: Py<PyAny>,
        mode: Option<Py<PyAny>>,
        create_parent_dir: Option<bool>,
        dir_mode: Option<Py<PyAny>>,
    ) -> PyResult<()> {
        let t = &slf.borrow().0;
        let mut file = PyBinaryFile::from(file);
        py.detach(|| {
            t.put_file_non_atomic(
                path,
                &mut file,
                mode.and_then(perms_from_py_object).or_else(default_perms),
                create_parent_dir,
                dir_mode.and_then(perms_from_py_object),
            )
        })
        .map_err(|e| {
            let obj = slf.clone().unbind().into();
            map_transport_err_to_py_err(e, Some(obj), Some(path))
        })?;
        Ok(())
    }

    fn delete(&self, py: Python, path: &str) -> PyResult<()> {
        let t = &self.0;
        py.detach(|| t.delete(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))?;
        Ok(())
    }

    fn rmdir(&self, py: Python, path: &str) -> PyResult<()> {
        let t = &self.0;

        py.detach(|| t.rmdir(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))?;
        Ok(())
    }

    fn rename(&self, py: Python, from: &str, to: &str) -> PyResult<()> {
        let t = &self.0;

        py.detach(|| t.rename(from, to))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(from)))?;
        Ok(())
    }

    #[pyo3(signature = (name, value=None))]
    fn set_segment_parameter(
        &mut self,
        py: Python,
        name: &str,
        value: Option<&str>,
    ) -> PyResult<()> {
        py.detach(|| self.0.set_segment_parameter(name, value))
            .map_err(|e| map_transport_err_to_py_err(e, None, None))?;
        Ok(())
    }

    fn get_segment_parameters(&self, py: Python) -> PyResult<HashMap<String, String>> {
        let t = &self.0;

        py.detach(|| t.get_segment_parameters())
            .map_err(|e| map_transport_err_to_py_err(e, None, None))
    }

    #[pyo3(signature = (mode=None))]
    fn create_prefix(&self, py: Python, mode: Option<Py<PyAny>>) -> PyResult<()> {
        let t = &self.0;

        py.detach(|| t.create_prefix(mode.and_then(perms_from_py_object)))
            .map_err(|e| map_transport_err_to_py_err(e, None, None))
    }

    fn lock_write(&self, py: Python, path: &str) -> PyResult<Lock> {
        let t = &self.0;

        py.detach(|| t.lock_write(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
            .map(Lock::from)
    }

    fn lock_read(&self, py: Python, path: &str) -> PyResult<Lock> {
        let t = &self.0;

        py.detach(|| t.lock_read(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
            .map(Lock::from)
    }

    fn recommended_page_size(&self, py: Python) -> usize {
        py.detach(|| self.0.recommended_page_size())
    }

    fn is_readonly(&self, py: Python) -> bool {
        py.detach(|| self.0.is_readonly())
    }

    #[pyo3(signature = (path, offsets, max_readv_combine=None, bytes_to_read_before_seek=None))]
    fn _readv<'a>(
        slf: &Bound<'a, Self>,
        py: Python<'a>,
        path: &str,
        offsets: Vec<(usize, usize)>,
        max_readv_combine: Option<usize>,
        bytes_to_read_before_seek: Option<usize>,
    ) -> PyResult<Bound<'a, PyAny>> {
        if offsets.is_empty() {
            return Ok(PyList::empty(py).into_any());
        }
        let t = &slf.borrow().0;
        let ret = py.detach(|| t.get(path)).map_err(|e| match e {
            Error::IsADirectoryError(_) => {
                ReadError::new_err((path.to_string(), "Is a directory".to_string()))
            }
            Error::NotADirectoryError(_) => {
                ReadError::new_err((path.to_string(), "Not a directory".to_string()))
            }
            e => {
                let obj = slf.clone().unbind().into();
                map_transport_err_to_py_err(e, Some(obj), Some(path))
            }
        })?;
        let f = Bound::new(py, PyBufReadStream::new(ret, Path::new(path)))?;
        let buffered = seek_and_read(
            py,
            f.into_any(),
            offsets,
            max_readv_combine,
            bytes_to_read_before_seek,
            Some(path),
        )?;
        let list = PyList::new(py, &buffered)?;
        Ok(PyIterator::from_object(&list.into_any())?.into_any())
    }

    #[pyo3(signature = (path, offsets, adjust_for_latency=None, upper_limit=None))]
    fn readv<'a>(
        slf: PyRef<'a, Self>,
        py: Python<'a>,
        path: String,
        offsets: Vec<(u64, usize)>,
        adjust_for_latency: Option<bool>,
        upper_limit: Option<u64>,
    ) -> PyResult<Py<ReadvIter>> {
        // Construct the Rust iterator up front — that step only
        // does coalescing, no HTTP, so running it with the GIL
        // held is fine. The returned iterator borrows the
        // transport's `dyn TransportTrait` but our HttpTransport
        // readv constructs a LazyReadv that owns its state (no
        // borrow of `self`); promote to `'static` via transmute
        // and keep the parent transport alive via a Python
        // reference held inside `ReadvIter`. Subsequent `__next__`
        // calls then pull elements under `py.detach` so blocking
        // HTTP work doesn't hold the GIL.
        let boxed: Box<dyn Iterator<Item = _> + Send + '_> = slf.0.readv(
            &path,
            offsets,
            adjust_for_latency.unwrap_or(false),
            upper_limit,
        );
        let mut iter: Box<dyn Iterator<Item = Result<(u64, Vec<u8>), dromedary::Error>> + Send> = unsafe {
            std::mem::transmute::<
                Box<dyn Iterator<Item = _> + Send + '_>,
                Box<dyn Iterator<Item = _> + Send + 'static>,
            >(boxed)
        };
        // Peek the first element synchronously so errors that the
        // transport raises up-front (NoSuchFile for a missing
        // file, PermissionDenied, &c) surface from the `readv()`
        // *call* itself, not from the first `__next__` on the
        // returned iterator. Python's Transport.readv contract
        // historically raised there — breezy's pack-repo autopack
        // (see `test_autopack_reloads_and_stops`) catches those
        // around `make_readv_reader` construction to translate
        // deleted-pack races into RetryWithNewPacks.
        let first = py.detach(|| iter.next());
        if let Some(Err(e)) = first {
            return Err(map_transport_err_to_py_err(e, None, Some(&path)));
        }
        let prefixed: Box<dyn Iterator<Item = Result<(u64, Vec<u8>), dromedary::Error>> + Send> =
            Box::new(first.into_iter().chain(iter));
        Py::new(
            py,
            ReadvIter {
                parent: slf.into_pyobject(py)?.unbind(),
                iter: Mutex::new(Some(prefixed)),
                path,
            },
        )
    }

    fn listable(&self, py: Python) -> bool {
        py.detach(|| self.0.listable())
    }

    fn list_dir(&self, py: Python, path: &str) -> PyResult<Vec<String>> {
        py.detach(|| {
            self.0
                .list_dir(path)
                .map(|r| r.map_err(|e| map_transport_err_to_py_err(e, None, Some(path))))
                .collect::<PyResult<Vec<_>>>()
        })
    }

    #[pyo3(signature = (path, bytes, mode=None))]
    fn append_bytes(
        &self,
        py: Python,
        path: &str,
        bytes: &[u8],
        mode: Option<Py<PyAny>>,
    ) -> PyResult<u64> {
        let mode = mode.and_then(perms_from_py_object);
        py.detach(|| self.0.append_bytes(path, bytes, mode))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
    }

    #[pyo3(signature = (path, file, mode=None))]
    fn append_file(
        &self,
        py: Python,
        path: &str,
        file: Py<PyAny>,
        mode: Option<Py<PyAny>>,
    ) -> PyResult<u64> {
        let mut file = PyBinaryFile::from(file);
        let mode = mode.and_then(perms_from_py_object);
        py.detach(|| self.0.append_file(path, &mut file, mode))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
    }

    fn iter_files_recursive<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyList>> {
        self.0
            .iter_files_recursive()
            .map(|r| {
                r.map_err(|e| map_transport_err_to_py_err(e, None, Some(".")))
                    .map(|o| o.to_string())
            })
            .collect::<PyResult<Vec<_>>>()
            .and_then(move |v| PyList::new(py, &v))
    }

    #[pyo3(signature = (path, mode=None))]
    fn open_write_stream(
        slf: &Bound<Self>,
        py: Python,
        path: &str,
        mode: Option<Py<PyAny>>,
    ) -> PyResult<PyWriteStream> {
        let t = &slf.borrow().0;
        py.detach(|| t.open_write_stream(path, mode.and_then(perms_from_py_object)))
            .map_err(|e| Transport::map_to_py_err(slf.borrow(), py, e, Some(path)))
            .map(|w| PyWriteStream(Some(w)))
    }

    fn delete_tree(&self, py: Python, path: &str) -> PyResult<()> {
        py.detach(|| self.0.delete_tree(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
    }

    fn r#move(&self, py: Python, from: &str, to: &str) -> PyResult<()> {
        py.detach(|| self.0.r#move(from, to))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(from)))
    }

    fn copy_tree(&self, py: Python, from: &str, to: &str) -> PyResult<()> {
        py.detach(|| self.0.copy_tree(from, to))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(from)))
    }

    fn copy_tree_to_transport(&self, py: Python, to_transport: Py<PyAny>) -> PyResult<()> {
        if let Ok(t) = to_transport.clone_ref(py).extract::<PyRef<Transport>>(py) {
            let t = t.0.as_ref();
            py.detach(|| self.0.copy_tree_to_transport(t))
                .map_err(|e| map_transport_err_to_py_err(e, None, Some(".")))
        } else {
            let t = Box::new(dromedary::pyo3::PyTransport::from(to_transport));
            py.detach(|| self.0.copy_tree_to_transport(t.as_ref()))
                .map_err(|e| map_transport_err_to_py_err(e, None, Some(".")))
        }
    }

    fn hardlink(&self, py: Python, from: &str, to: &str) -> PyResult<()> {
        py.detach(|| self.0.hardlink(from, to))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(from)))
    }

    fn symlink(&self, py: Python, from: &str, to: &str) -> PyResult<()> {
        py.detach(|| self.0.symlink(from, to))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(from)))
    }

    fn readlink(&self, py: Python, path: &str) -> PyResult<String> {
        py.detach(|| self.0.readlink(path))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(path)))
    }

    #[pyo3(signature = (relpaths, to_transport, mode=None))]
    fn copy_to(
        &self,
        py: Python,
        relpaths: Py<PyAny>,
        to_transport: Py<PyAny>,
        mode: Option<Py<PyAny>>,
    ) -> PyResult<usize> {
        let relpaths = relpaths
            .bind(py)
            .try_iter()?
            .map(|o| o?.extract::<String>())
            .collect::<PyResult<Vec<_>>>()?;

        let relpaths_ref = relpaths.iter().map(|s| s.as_str()).collect::<Vec<_>>();

        if let Ok(t) = to_transport.clone_ref(py).cast_bound::<Transport>(py) {
            let t = &t.borrow().0;
            py.detach(|| {
                self.0.copy_to(
                    relpaths_ref.as_slice(),
                    t.as_ref(),
                    mode.and_then(perms_from_py_object),
                )
            })
            .map_err(|e| map_transport_err_to_py_err(e, None, None))
        } else {
            let t = Box::new(dromedary::pyo3::PyTransport::from(to_transport));
            py.detach(|| {
                self.0
                    .copy_to(
                        relpaths_ref.as_slice(),
                        t.as_ref(),
                        mode.and_then(perms_from_py_object),
                    )
                    .map_err(|e| map_transport_err_to_py_err(e, None, None))
            })
        }
    }

    fn _can_roundtrip_unix_modebits(&self) -> bool {
        self.0.can_roundtrip_unix_modebits()
    }

    fn copy(&self, py: Python, from: &str, to: &str) -> PyResult<()> {
        py.detach(|| self.0.copy(from, to))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(from)))
    }
}

#[pyclass]
struct Lock(Box<dyn dromedary::lock::Lock + Send + Sync>);

impl From<Box<dyn dromedary::lock::Lock + Send + Sync>> for Lock {
    fn from(lock: Box<dyn dromedary::lock::Lock + Send + Sync>) -> Self {
        Lock(lock)
    }
}

#[pymethods]
impl Lock {
    fn unlock(&mut self) -> PyResult<()> {
        self.0.unlock().map_err(map_lock_err_to_py_err)
    }
}

#[pyclass(extends=Transport,subclass)]
struct LocalTransport {}

#[pymethods]
impl LocalTransport {
    #[new]
    fn new(url: &str) -> PyResult<(Self, Transport)> {
        Ok((
            LocalTransport {},
            Transport(Box::new(
                dromedary::local::LocalTransport::new(url)
                    .map_err(|e| map_transport_err_to_py_err(e, None, None))?,
            )),
        ))
    }

    #[pyo3(signature = (abspath,))]
    #[classmethod]
    fn from_abspath<'a>(cls: &'a Bound<'a, PyType>, abspath: &'a str) -> PyResult<Bound<'a, Self>> {
        let ret = dromedary::local::LocalTransport::from_abspath(Path::new(abspath))
            .map_err(|e| map_transport_err_to_py_err(e, None, Some(abspath)))?;

        let init = PyClassInitializer::from(Transport(Box::new(ret)));
        let init = init.add_subclass(Self {});
        Bound::new(cls.py(), init)
    }

    #[pyo3(signature = (offset=None))]
    fn clone<'a>(
        slf: PyRef<'a, Self>,
        py: Python<'a>,
        offset: Option<Py<PyAny>>,
    ) -> PyResult<Bound<'a, LocalTransport>> {
        let super_ = slf.as_ref();
        let inner = if let Some(offset) = offset {
            let offset = offset.extract::<String>(py)?;
            super_.0.clone(Some(&offset))
        } else {
            super_.0.clone(None)
        }
        .map_err(|e| map_transport_err_to_py_err(e, None, None))?;

        let init = PyClassInitializer::from(Transport(inner));
        let init = init.add_subclass(Self {});
        Bound::new(py, init)
    }
}

#[pyfunction]
fn get_test_permutations(py: Python) -> PyResult<Bound<PyList>> {
    let test_server_module = py.import("dromedary.tests.test_server")?;
    let local_url_server = test_server_module.getattr("LocalURLServer")?;
    let local_transport = py.import("dromedary.local")?.getattr("LocalTransport")?;
    let ret = PyList::empty(py);
    ret.append((local_transport, local_url_server))?;
    Ok(ret)
}

#[pyfunction]
#[pyo3(signature = (offsets, limit=None, fudge_factor=None, max_size=None))]
fn coalesce_offsets(
    offsets: Vec<(usize, usize)>,
    mut limit: Option<usize>,
    mut fudge_factor: Option<usize>,
    mut max_size: Option<usize>,
) -> PyResult<Vec<(usize, usize, Vec<(usize, usize)>)>> {
    if limit == Some(0) {
        limit = None;
    }
    if fudge_factor == Some(0) {
        fudge_factor = None;
    }
    if max_size == Some(0) {
        max_size = None;
    }
    dromedary::readv::coalesce_offsets(offsets.as_slice(), limit, fudge_factor, max_size)
        .map_err(|e| PyValueError::new_err(format!("{}", e)))
}

const DEFAULT_MAX_READV_COMBINE: usize = 50;
const DEFAULT_BYTES_TO_READ_BEFORE_SEEK: usize = 0;

#[pyfunction]
#[pyo3(signature = (file, offsets, max_readv_combine=None, bytes_to_read_before_seek=None, path=None))]
fn seek_and_read(
    py: Python,
    file: Bound<PyAny>,
    offsets: Vec<(usize, usize)>,
    max_readv_combine: Option<usize>,
    bytes_to_read_before_seek: Option<usize>,
    path: Option<&str>,
) -> PyResult<Vec<(usize, Py<PyAny>)>> {
    let f = PyBinaryFile::from(file);
    let mut data = py
        .detach(|| {
            dromedary::readv::seek_and_read(
                f,
                offsets,
                max_readv_combine.unwrap_or(DEFAULT_MAX_READV_COMBINE),
                bytes_to_read_before_seek.unwrap_or(DEFAULT_BYTES_TO_READ_BEFORE_SEEK),
            )
        })
        .map_err(|e| -> PyErr { e.into() })?;

    std::iter::from_fn(move || py.detach(|| data.next()))
        .map(|e| {
            e.map(|(offset, data)| (offset, PyBytes::new(py, data.as_slice()).into()))
                .map_err(|(e, offset, length, actual)| match e.kind() {
                    std::io::ErrorKind::UnexpectedEof => ShortReadvError::new_err((
                        path.map(|p| p.to_string()),
                        offset,
                        length,
                        actual,
                    )),
                    _ => e.into(),
                })
        })
        .collect::<PyResult<Vec<_>>>()
}

#[pyfunction]
#[pyo3(signature = (offsets, upper_limit=None, recommended_page_size=None))]
fn sort_expand_and_combine(
    offsets: Vec<(u64, usize)>,
    upper_limit: Option<u64>,
    recommended_page_size: Option<usize>,
) -> Vec<(u64, usize)> {
    dromedary::readv::sort_expand_and_combine(
        offsets,
        upper_limit,
        recommended_page_size.unwrap_or(4 * 1024),
    )
}

#[pyclass]
struct PyFile(BufReader<Box<std::fs::File>>);

impl PyFile {
    fn new(f: Box<std::fs::File>, _path: &Path) -> Self {
        Self(BufReader::new(f))
    }
}

#[pymethods]
impl PyFile {
    fn seekable(&self) -> bool {
        true
    }

    #[pyo3(signature = (size=None))]
    fn read<'a>(&mut self, py: Python<'a>, size: Option<usize>) -> PyResult<Bound<'a, PyBytes>> {
        if let Some(size) = size {
            let mut buf = vec![0; size];
            let ret = py
                .detach(|| self.0.read(&mut buf))
                .map_err(|e| -> PyErr { e.into() })?;
            Ok(PyBytes::new(py, &buf[..ret]))
        } else {
            let mut buf = Vec::new();
            py.detach(|| self.0.read_to_end(&mut buf))
                .map_err(|e| -> PyErr { e.into() })?;
            Ok(PyBytes::new(py, &buf))
        }
    }

    fn write(&mut self, py: Python, data: &[u8]) -> PyResult<usize> {
        py.detach(|| self.0.get_mut().write(data))
            .map_err(|e| e.into())
    }

    fn readline<'a>(&mut self, py: Python<'a>) -> PyResult<Bound<'a, PyBytes>> {
        let mut buf = vec![];
        let ret = py.detach(|| self.0.read_until(b'\n', &mut buf))?;
        buf.truncate(ret);
        Ok(PyBytes::new(py, &buf))
    }

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

    fn __next__<'a>(&mut self, py: Python<'a>) -> PyResult<Option<Bound<'a, PyBytes>>> {
        let mut buf = vec![];
        let ret = py
            .detach(|| self.0.read_until(b'\n', &mut buf))
            .map_err(|e| -> PyErr { e.into() })?;
        if ret == 0 {
            return Ok(None);
        }
        buf.truncate(ret);
        Ok(Some(PyBytes::new(py, &buf)))
    }

    fn readlines<'a>(&mut self, py: Python<'a>) -> PyResult<Bound<'a, PyList>> {
        let ret = PyList::empty(py);
        while let Some(line) = self.__next__(py)? {
            ret.append(line)?;
        }
        Ok(ret)
    }

    #[pyo3(signature = (offset, whence=None))]
    fn seek(&mut self, offset: i64, whence: Option<i8>) -> PyResult<u64> {
        let seekfrom = match whence.unwrap_or(0) {
            0 => std::io::SeekFrom::Start(offset as u64),
            1 => std::io::SeekFrom::Current(offset),
            2 => std::io::SeekFrom::End(offset),
            _ => return Err(PyValueError::new_err("Invalid whence")),
        };

        self.0.seek(seekfrom).map_err(|e| e.into())
    }

    fn tell(&mut self) -> PyResult<u64> {
        self.0.stream_position().map_err(|e| e.into())
    }

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

    fn __exit__(
        &self,
        _exc_type: Option<&Bound<PyType>>,
        _exc_val: Option<&Bound<PyAny>>,
        _exc_tb: Option<&Bound<PyAny>>,
    ) -> PyResult<bool> {
        Ok(false)
    }

    fn flush(&mut self) -> PyResult<()> {
        self.0.get_mut().flush().map_err(|e| e.into())
    }

    fn writelines(&mut self, py: Python, lines: &Bound<PyList>) -> PyResult<()> {
        for line in lines.iter() {
            self.write(py, line.extract::<&[u8]>()?)?;
        }
        Ok(())
    }

    #[pyo3(signature = (size=None))]
    fn truncate(&mut self, py: Python, size: Option<u64>) -> PyResult<()> {
        let size = size.map_or_else(|| py.detach(|| self.tell()), Ok)?;
        py.detach(|| self.0.get_mut().set_len(size))
            .map_err(|e| e.into())
    }

    #[cfg(unix)]
    fn fileno(&self, py: Python) -> PyResult<i32> {
        use std::os::unix::io::AsRawFd;
        Ok(py.detach(|| self.0.get_ref().as_raw_fd()))
    }
}

fn map_lock_err_to_py_err(err: LockError) -> PyErr {
    match err {
        LockError::Contention(p) => LockContention::new_err((p,)),
        LockError::Failed(p, w) => LockFailed::new_err((p, w)),
        LockError::IoError(e) => e.into(),
    }
}

#[pyclass]
struct ReadLock(Option<dromedary::filelock::ReadLock>);

#[pyclass]
struct WriteLock(dromedary::filelock::WriteLock);

#[pymethods]
impl ReadLock {
    fn unlock(&mut self) -> PyResult<()> {
        if let Some(mut read_lock) = self.0.take() {
            read_lock.unlock().map_err(map_lock_err_to_py_err)
        } else {
            debug!("ReadLock already unlocked");
            Ok(())
        }
    }

    #[new]
    #[pyo3(signature = (filename, strict_locks=None))]
    fn new(filename: PathBuf, strict_locks: Option<bool>) -> PyResult<Self> {
        Ok(Self(Some(
            dromedary::filelock::ReadLock::new(&filename, strict_locks.unwrap_or(false))
                .map_err(map_lock_err_to_py_err)?,
        )))
    }

    fn temporary_write_lock<'a>(
        slf: Bound<'a, Self>,
        py: Python<'a>,
    ) -> PyResult<(bool, Bound<'a, PyAny>)> {
        let mut m = slf.borrow_mut();
        if let Some(read_lock) = m.0.take() {
            match read_lock.temporary_write_lock() {
                Ok(twl) => Ok((
                    true,
                    Bound::new(py, TemporaryWriteLock(Some(twl)))?.into_any(),
                )),
                Err((rl, LockError::Contention(_))) => {
                    m.0 = Some(rl);
                    Ok((false, slf.into_any()))
                }
                Err((_rl, LockError::Failed(p, w))) => Err(LockFailed::new_err((p, w))),
                Err((_rl, LockError::IoError(e))) => Err(e.into()),
            }
        } else {
            Err(PyRuntimeError::new_err("ReadLock already unlocked"))
        }
    }

    #[getter]
    fn f(&self) -> PyResult<PyFile> {
        if let Some(read_lock) = &self.0 {
            Ok(PyFile::new(read_lock.file()?, read_lock.path()))
        } else {
            Err(PyRuntimeError::new_err("ReadLock already unlocked"))
        }
    }
}

#[pyclass]
struct TemporaryWriteLock(Option<dromedary::filelock::TemporaryWriteLock>);

#[pymethods]
impl TemporaryWriteLock {
    fn restore_read_lock(&mut self) -> PyResult<ReadLock> {
        if let Some(lock) = self.0.take() {
            let rl = lock.restore_read_lock();
            Ok(ReadLock(Some(rl)))
        } else {
            Err(PyRuntimeError::new_err(
                "TemporaryWriteLock already unlocked",
            ))
        }
    }

    #[getter]
    fn f(&self) -> PyResult<PyFile> {
        if let Some(lock) = &self.0 {
            Ok(PyFile::new(lock.file()?, lock.path()))
        } else {
            Err(PyRuntimeError::new_err(
                "TemporaryWriteLock already unlocked",
            ))
        }
    }
}

#[pymethods]
impl WriteLock {
    fn unlock(&mut self) -> PyResult<()> {
        self.0.unlock().map_err(map_lock_err_to_py_err)
    }

    #[new]
    #[pyo3(signature = (filename, strict_locks=None))]
    fn new(filename: PathBuf, strict_locks: Option<bool>) -> PyResult<Self> {
        Ok(Self(
            dromedary::filelock::WriteLock::new(&filename, strict_locks.unwrap_or(false))
                .map_err(map_lock_err_to_py_err)?,
        ))
    }

    #[getter]
    fn f(&self) -> PyResult<PyFile> {
        Ok(PyFile::new(self.0.file()?, self.0.path()))
    }
}

mod brokenrename;
mod fakenfs;
mod fakevfat;
#[cfg(feature = "gio")]
mod gio;
mod http;
mod log;
mod memory;
mod pathfilter;
mod readonly;
mod sftp;
mod ssh;
mod unlistable;
mod urlutils;
#[cfg(feature = "webdav")]
mod webdav;

#[pymodule]
fn _transport_rs(py: Python, m: &Bound<PyModule>) -> PyResult<()> {
    m.add_class::<Transport>()?;
    m.add_class::<ConnectedTransport>()?;
    m.add_class::<ReadvIter>()?;
    m.add_class::<TransportDecorator>()?;
    let localm = PyModule::new(py, "local")?;
    localm.add_class::<LocalTransport>()?;
    m.add_submodule(&localm)?;
    m.add_class::<ReadLock>()?;
    m.add_class::<WriteLock>()?;
    m.add_class::<TemporaryWriteLock>()?;
    m.add_function(wrap_pyfunction!(get_test_permutations, m)?)?;
    m.add_wrapped(wrap_pyfunction!(seek_and_read))?;
    m.add_wrapped(wrap_pyfunction!(coalesce_offsets))?;
    m.add_wrapped(wrap_pyfunction!(sort_expand_and_combine))?;

    let sftpm = PyModule::new(py, "sftp")?;
    sftp::_sftp_rs(py, &sftpm)?;
    m.add_submodule(&sftpm)?;

    let sshm = PyModule::new(py, "ssh")?;
    ssh::register(py, &sshm)?;
    m.add_submodule(&sshm)?;
    m.add_class::<ReadLock>()?;
    m.add_class::<WriteLock>()?;
    m.add_class::<TemporaryWriteLock>()?;

    let urlutilsm = PyModule::new(py, "urlutils")?;
    urlutils::_urlutils_rs(py, &urlutilsm)?;
    m.add_submodule(&urlutilsm)?;

    let unlistablem = PyModule::new(py, "unlistable")?;
    unlistable::register(py, &unlistablem)?;
    m.add_submodule(&unlistablem)?;

    let readonlym = PyModule::new(py, "readonly")?;
    readonly::register(py, &readonlym)?;
    m.add_submodule(&readonlym)?;

    let brokenrenamem = PyModule::new(py, "brokenrename")?;
    brokenrename::register(py, &brokenrenamem)?;
    m.add_submodule(&brokenrenamem)?;

    let fakevfatm = PyModule::new(py, "fakevfat")?;
    fakevfat::register(py, &fakevfatm)?;
    m.add_submodule(&fakevfatm)?;

    let fakenfsm = PyModule::new(py, "fakenfs")?;
    fakenfs::register(py, &fakenfsm)?;
    m.add_submodule(&fakenfsm)?;

    let pathfilterm = PyModule::new(py, "pathfilter")?;
    pathfilter::register(py, &pathfilterm)?;
    m.add_submodule(&pathfilterm)?;

    let memorym = PyModule::new(py, "memory")?;
    memory::register(py, &memorym)?;
    m.add_submodule(&memorym)?;

    let httpm = PyModule::new(py, "http")?;
    http::register(py, &httpm)?;
    m.add_submodule(&httpm)?;

    #[cfg(feature = "webdav")]
    let webdavm = {
        let webdavm = PyModule::new(py, "webdav")?;
        webdav::register(&webdavm)?;
        m.add_submodule(&webdavm)?;
        webdavm
    };

    let logm = PyModule::new(py, "log")?;
    log::register(py, &logm)?;
    m.add_submodule(&logm)?;

    #[cfg(feature = "gio")]
    let giom = {
        let giom = PyModule::new(py, "gio")?;
        gio::register(py, &giom)?;
        m.add_submodule(&giom)?;
        giom
    };

    // PyO3 submodule hack for proper import support
    let sys = py.import("sys")?;
    let modules = sys.getattr("modules")?;
    let module_name = m.name()?;

    // Register submodules in sys.modules for dotted import support
    modules.set_item(format!("{}.local", module_name), &localm)?;
    modules.set_item(format!("{}.sftp", module_name), &sftpm)?;
    modules.set_item(format!("{}.ssh", module_name), &sshm)?;
    modules.set_item(format!("{}.urlutils", module_name), &urlutilsm)?;
    modules.set_item(format!("{}.unlistable", module_name), &unlistablem)?;
    modules.set_item(format!("{}.readonly", module_name), &readonlym)?;
    modules.set_item(format!("{}.brokenrename", module_name), &brokenrenamem)?;
    modules.set_item(format!("{}.fakevfat", module_name), &fakevfatm)?;
    modules.set_item(format!("{}.fakenfs", module_name), &fakenfsm)?;
    modules.set_item(format!("{}.pathfilter", module_name), &pathfilterm)?;
    modules.set_item(format!("{}.memory", module_name), &memorym)?;
    modules.set_item(format!("{}.http", module_name), &httpm)?;
    #[cfg(feature = "webdav")]
    modules.set_item(format!("{}.webdav", module_name), &webdavm)?;
    modules.set_item(format!("{}.log", module_name), &logm)?;
    #[cfg(feature = "gio")]
    modules.set_item(format!("{}.gio", module_name), &giom)?;

    Ok(())
}
