use crate::{
    Error, Lock, LockError, ReadStream, Result, SmartMedium, Stat, Transport, Url, UrlFragment,
    WriteStream,
};
use pyo3::import_exception;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use pyo3_filelike::PyBinaryFile;
use std::collections::HashMap;
use std::fs::Permissions;
use std::io::{Read, Write};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

/// Convert `Option<Permissions>` to the `Optional[int]` mode expected by the
/// Python transport API. On Windows Python itself ignores the mode argument
/// (chmod there only touches the read-only bit), so we pass `None`.
#[inline]
fn perms_to_py_mode(perms: Option<&Permissions>) -> Option<u32> {
    #[cfg(unix)]
    {
        perms.map(|p| p.mode())
    }
    #[cfg(not(unix))]
    {
        let _ = perms;
        None
    }
}

import_exception!(dromedary.errors, TransportError);
import_exception!(dromedary.errors, NoSmartMedium);
import_exception!(dromedary.errors, InProcessTransport);
import_exception!(dromedary.errors, NotLocalUrl);
import_exception!(dromedary.errors, NoSuchFile);
import_exception!(dromedary.errors, FileExists);
import_exception!(dromedary.errors, TransportNotPossible);
import_exception!(dromedary.errors, UrlError);
import_exception!(dromedary.errors, PermissionDenied);
import_exception!(dromedary.errors, PathNotChild);
import_exception!(dromedary.errors, ShortReadvError);
import_exception!(dromedary.errors, LockContention);
import_exception!(dromedary.errors, LockFailed);
import_exception!(dromedary.errors, DirectoryNotEmpty);
import_exception!(dromedary.errors, NotADirectory);
import_exception!(dromedary.errors, ResourceBusy);
import_exception!(dromedary.errors, ReadError);
import_exception!(dromedary.urlutils, InvalidURL);

struct PySmartMedium(Py<PyAny>);

impl SmartMedium for PySmartMedium {}

pub struct PyTransport(Py<PyAny>);

impl From<Py<PyAny>> for PyTransport {
    fn from(obj: Py<PyAny>) -> Self {
        PyTransport(obj)
    }
}

fn map_py_err_to_lock_err(e: PyErr) -> LockError {
    Python::attach(|py| {
        if e.is_instance_of::<LockContention>(py) {
            LockError::Contention(e.value(py).getattr("lock").unwrap().extract().unwrap())
        } else if e.is_instance_of::<LockFailed>(py) {
            let v = e.value(py);
            LockError::Failed(
                v.getattr("lock").unwrap().extract().unwrap(),
                v.getattr("why").unwrap().extract().unwrap(),
            )
        } else {
            LockError::IoError(e.into())
        }
    })
}

struct PyLock(Py<PyAny>);

impl Lock for PyLock {
    fn unlock(&mut self) -> std::result::Result<(), LockError> {
        Python::attach(|py| {
            self.0
                .call_method0(py, "unlock")
                .map_err(map_py_err_to_lock_err)?;
            Ok(())
        })
    }
}

impl<'py> IntoPyObject<'py> for PyTransport {
    type Target = PyAny;

    type Output = Bound<'py, Self::Target>;

    type Error = PyErr;

    fn into_pyobject(self, py: Python<'py>) -> std::result::Result<Self::Output, Self::Error> {
        Ok(self.0.bind(py).clone())
    }
}

impl From<PyErr> for Error {
    fn from(e: PyErr) -> Self {
        Python::attach(|py| {
            let arg = |_i| -> Option<String> {
                let args = e.value(py).getattr("args").ok()?;
                let item = args.get_item(0).ok()?;
                if item.is_none() {
                    None
                } else {
                    item.extract::<String>().ok()
                }
            };
            if e.is_instance_of::<InProcessTransport>(py) {
                Error::InProcessTransport
            } else if e.is_instance_of::<NotLocalUrl>(py) {
                let url = e
                    .value(py)
                    .getattr("url")
                    .ok()
                    .and_then(|u| u.extract::<String>().ok())
                    .unwrap_or_default();
                Error::NotLocalUrl(url)
            } else if e.is_instance_of::<NoSuchFile>(py) {
                Error::NoSuchFile(arg(0))
            } else if e.is_instance_of::<FileExists>(py) {
                Error::FileExists(arg(0))
            } else if e.is_instance_of::<TransportNotPossible>(py) {
                Error::TransportNotPossible
            } else if e.is_instance_of::<PermissionDenied>(py) {
                Error::PermissionDenied(arg(0))
            } else if e.is_instance_of::<PathNotChild>(py) {
                Error::PathNotChild
            } else if e.is_instance_of::<DirectoryNotEmpty>(py) {
                Error::DirectoryNotEmptyError(arg(0))
            } else if e.is_instance_of::<NotADirectory>(py) {
                Error::NotADirectoryError(arg(0))
            } else if e.is_instance_of::<ResourceBusy>(py) {
                Error::ResourceBusy(arg(0))
            } else if e.is_instance_of::<ReadError>(py) {
                Error::IsADirectoryError(arg(0))
            } else if e.is_instance_of::<InvalidURL>(py) {
                Error::UrlutilsError(crate::urlutils::Error::UrlNotAscii(
                    arg(0).unwrap_or_default(),
                ))
            } else if e.is_instance_of::<ShortReadvError>(py) {
                let value = e.value(py);
                Error::ShortReadvError(
                    value.getattr("path").unwrap().extract::<String>().unwrap(),
                    value.getattr("offset").unwrap().extract::<u64>().unwrap(),
                    value.getattr("length").unwrap().extract::<u64>().unwrap(),
                    value.getattr("actual").unwrap().extract::<u64>().unwrap(),
                )
            } else {
                // Don't panic on unrecognised exception types — funnel them
                // through Error::Io so the caller sees something useful and
                // the worker stays alive. New variants should still be added
                // explicitly above when they have a real semantic mapping.
                Error::Io(std::io::Error::other(e.to_string()))
            }
        })
    }
}

impl ReadStream for PyBinaryFile {}

// Bit of a hack - this reads the entire buffer, and then streams it
fn py_read(r: &mut dyn Read) -> PyResult<Py<PyAny>> {
    Python::attach(|py| {
        let mut buffer = Vec::new();
        r.read_to_end(&mut buffer)?;
        let io = py.import("io")?;
        let bytesio = io.getattr("BytesIO")?;
        Ok(bytesio.call1((buffer,))?.unbind())
    })
}

struct PyWriteStream(Py<PyAny>);

impl Write for PyWriteStream {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "write", (buf,))?;
            Ok(obj.extract::<usize>(py)?)
        })
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Python::attach(|py| {
            self.0.call_method0(py, "flush")?;
            Ok(())
        })
    }
}

impl WriteStream for PyWriteStream {
    fn sync_data(&self) -> std::io::Result<()> {
        Python::attach(|py| {
            self.0.call_method0(py, "fdatasync")?;
            Ok(())
        })
    }
}

impl std::fmt::Debug for PyTransport {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "PyTransport({:?})", self.0)
    }
}

impl Transport for PyTransport {
    fn external_url(&self) -> Result<Url> {
        Python::attach(|py| {
            let obj = self.0.call_method0(py, "external_url")?;
            let s = obj.extract::<String>(py)?;
            Url::parse(&s).map_err(Error::from)
        })
    }

    fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "get_bytes", (path,))?;
            let bytes = obj.cast_bound::<PyBytes>(py).map_err(PyErr::from)?;
            Ok(bytes.as_bytes().to_vec())
        })
    }

    fn get(&self, path: &str) -> Result<Box<dyn ReadStream + Send + Sync>> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "get", (path,))?;
            Ok(Box::new(PyBinaryFile::from(obj)) as Box<dyn ReadStream + Send + Sync>)
        })
    }

    fn base(&self) -> Url {
        Python::attach(|py| {
            // `.base` is a required attribute on every Transport. If the
            // wrapped Python object can't produce one we have no choice but
            // to fall back to a placeholder URL — none of the trait callers
            // expect this to fail.
            let url_str = self
                .0
                .getattr(py, "base")
                .ok()
                .and_then(|obj| obj.extract::<String>(py).ok())
                .unwrap_or_default();
            Url::parse(&url_str).unwrap_or_else(|_| Url::parse("file:///").unwrap())
        })
    }

    fn has(&self, path: &UrlFragment) -> Result<bool> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "has", (path,))?;
            Ok(obj.extract::<bool>(py)?)
        })
    }

    fn has_any(&self, paths: &[&UrlFragment]) -> Result<bool> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "has_any", (paths.to_vec(),))?;
            Ok(obj.extract::<bool>(py)?)
        })
    }

    fn mkdir(&self, relpath: &UrlFragment, perms: Option<Permissions>) -> Result<()> {
        Python::attach(|py| {
            self.0
                .call_method1(py, "mkdir", (relpath, perms_to_py_mode(perms.as_ref())))?;
            Ok(())
        })
    }

    fn ensure_base(&self, perms: Option<Permissions>) -> Result<bool> {
        Python::attach(|py| {
            let obj =
                self.0
                    .call_method1(py, "ensure_base", (perms_to_py_mode(perms.as_ref()),))?;
            Ok(obj.extract::<bool>(py)?)
        })
    }

    fn stat(&self, path: &UrlFragment) -> Result<Stat> {
        Python::attach(|py| {
            let stat_result = self.0.call_method1(py, "stat", (path,))?;

            let mtime = if let Ok(mtime) = stat_result.getattr(py, "mtime") {
                Some(mtime.extract::<f64>(py)?)
            } else {
                None
            };

            let st_mode = stat_result.getattr(py, "st_mode")?.extract::<u32>(py)?;
            // Derive kind from the POSIX mode bits Python reported.
            // On Windows Python still reports something meaningful for dir vs file.
            let kind = {
                const S_IFMT: u32 = 0o170000;
                const S_IFDIR: u32 = 0o040000;
                const S_IFREG: u32 = 0o100000;
                const S_IFLNK: u32 = 0o120000;
                match st_mode & S_IFMT {
                    S_IFDIR => crate::FileKind::Dir,
                    S_IFREG => crate::FileKind::File,
                    S_IFLNK => crate::FileKind::Symlink,
                    _ => crate::FileKind::Other,
                }
            };

            Ok(Stat {
                #[cfg(unix)]
                mode: st_mode,
                kind,
                size: stat_result.getattr(py, "st_size")?.extract::<usize>(py)?,
                mtime,
            })
        })
    }

    fn clone(&self, path: Option<&UrlFragment>) -> Result<Box<dyn Transport>> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "clone", (path,))?;
            let transport: Box<dyn Transport> = Box::new(PyTransport(obj));
            Ok(transport)
        })
    }

    fn relpath(&self, path: &Url) -> Result<String> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "relpath", (path.to_string(),))?;
            Ok(obj.extract::<String>(py)?)
        })
    }

    fn abspath(&self, relpath: &UrlFragment) -> Result<Url> {
        let s = Python::attach(|py| {
            let obj = self.0.call_method1(py, "abspath", (relpath,))?;
            obj.extract::<String>(py)
        })?;
        Url::parse(&s).map_err(Error::from)
    }

    fn put_file(
        &self,
        relpath: &UrlFragment,
        f: &mut dyn Read,
        mode: Option<Permissions>,
    ) -> Result<u64> {
        let f = py_read(f)?;
        Python::attach(|py| {
            let ret = self.0.call_method1(
                py,
                "put_file",
                (relpath, f, perms_to_py_mode(mode.as_ref())),
            )?;
            Ok(ret.extract::<u64>(py)?)
        })
    }

    fn put_bytes(
        &self,
        relpath: &UrlFragment,
        bytes: &[u8],
        mode: Option<Permissions>,
    ) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(
                py,
                "put_bytes",
                (relpath, bytes, perms_to_py_mode(mode.as_ref())),
            )?;
            Ok(())
        })
    }

    fn put_file_non_atomic(
        &self,
        relpath: &UrlFragment,
        f: &mut dyn Read,
        mode: Option<Permissions>,
        create_parent: Option<bool>,
        parent_mode: Option<Permissions>,
    ) -> Result<()> {
        let f = py_read(f)?;
        Python::attach(|py| {
            self.0.call_method1(
                py,
                "put_file_non_atomic",
                (
                    relpath,
                    f,
                    perms_to_py_mode(mode.as_ref()),
                    create_parent,
                    perms_to_py_mode(parent_mode.as_ref()),
                ),
            )?;
            Ok(())
        })
    }

    fn put_bytes_non_atomic(
        &self,
        relpath: &UrlFragment,
        bytes: &[u8],
        mode: Option<Permissions>,
        create_parent: Option<bool>,
        parent_mode: Option<Permissions>,
    ) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(
                py,
                "put_bytes_non_atomic",
                (
                    relpath,
                    bytes,
                    perms_to_py_mode(mode.as_ref()),
                    create_parent,
                    perms_to_py_mode(parent_mode.as_ref()),
                ),
            )?;
            Ok(())
        })
    }

    fn delete(&self, relpath: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "delete", (relpath,))?;
            Ok(())
        })
    }

    fn rmdir(&self, relpath: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "rmdir", (relpath,))?;
            Ok(())
        })
    }

    fn rename(&self, relpath: &UrlFragment, new_relpath: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "rename", (relpath, new_relpath))?;
            Ok(())
        })
    }

    fn set_segment_parameter(&mut self, key: &str, value: Option<&str>) -> Result<()> {
        Python::attach(|py| {
            self.0
                .call_method1(py, "set_segment_parameter", (key, value))?;
            Ok(())
        })
    }

    fn get_segment_parameters(&self) -> Result<HashMap<String, String>> {
        Python::attach(|py| {
            Ok(self
                .0
                .call_method0(py, "get_segment_parameters")?
                .extract::<HashMap<String, String>>(py)?)
        })
    }

    fn create_prefix(&self, permissions: Option<Permissions>) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(
                py,
                "create_prefix",
                (perms_to_py_mode(permissions.as_ref()),),
            )?;
            Ok(())
        })
    }

    fn recommended_page_size(&self) -> usize {
        Python::attach(|py| {
            self.0
                .call_method0(py, "recommended_page_size")
                .ok()
                .and_then(|obj| obj.extract::<usize>(py).ok())
                .unwrap_or(4 * 1024)
        })
    }

    fn is_readonly(&self) -> bool {
        Python::attach(|py| {
            self.0
                .call_method0(py, "is_readonly")
                .ok()
                .and_then(|obj| obj.extract::<bool>(py).ok())
                .unwrap_or(false)
        })
    }

    fn readv(
        &self,
        relpath: &UrlFragment,
        offsets: Vec<(u64, usize)>,
        adjust_for_latency: bool,
        upper_limit: Option<u64>,
    ) -> Box<dyn Iterator<Item = Result<(u64, Vec<u8>)>> + Send> {
        let iter = Python::attach(|py| -> Result<Py<PyAny>> {
            let raw = self.0.call_method1(
                py,
                "readv",
                (relpath, offsets, adjust_for_latency, upper_limit),
            )?;
            let it = raw.bind(py).try_iter().map_err(Error::from)?;
            Ok(it.unbind().into_any())
        });

        let iter = match iter {
            Ok(i) => i,
            Err(e) => return Box::new(std::iter::once(Err(e))),
        };

        Box::new(std::iter::from_fn(move || {
            Python::attach(|py| -> Option<Result<(u64, Vec<u8>)>> {
                match iter.call_method0(py, "__next__") {
                    Ok(obj) => {
                        if obj.is_none(py) {
                            None
                        } else {
                            match obj.extract::<(u64, Vec<u8>)>(py) {
                                Ok(pair) => Some(Ok(pair)),
                                Err(e) => Some(Err(Error::from(e))),
                            }
                        }
                    }
                    Err(e) if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) => None,
                    Err(e) => Some(Err(Error::from(e))),
                }
            })
        }))
    }

    fn append_bytes(
        &self,
        relpath: &UrlFragment,
        bytes: &[u8],
        permissions: Option<Permissions>,
    ) -> Result<u64> {
        Python::attach(|py| {
            let pos = self.0.call_method1(
                py,
                "append_bytes",
                (relpath, bytes, perms_to_py_mode(permissions.as_ref())),
            )?;
            Ok(pos.extract::<u64>(py)?)
        })
    }

    fn append_file(
        &self,
        relpath: &UrlFragment,
        f: &mut dyn Read,
        permissions: Option<Permissions>,
    ) -> Result<u64> {
        let f = py_read(f)?;
        Python::attach(|py| {
            let pos = self.0.call_method1(
                py,
                "append_file",
                (relpath, f, perms_to_py_mode(permissions.as_ref())),
            )?;
            Ok(pos.extract::<u64>(py)?)
        })
    }

    fn readlink(&self, relpath: &UrlFragment) -> Result<String> {
        Python::attach(|py| {
            Ok(self
                .0
                .call_method1(py, "readlink", (relpath,))?
                .extract::<String>(py)?)
        })
    }

    fn hardlink(&self, relpath: &UrlFragment, new_relpath: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0
                .call_method1(py, "hardlink", (relpath, new_relpath))?;
            Ok(())
        })
    }

    fn symlink(&self, relpath: &UrlFragment, new_relpath: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "symlink", (relpath, new_relpath))?;
            Ok(())
        })
    }

    fn iter_files_recursive(&self) -> Box<dyn Iterator<Item = Result<String>>> {
        let iter = Python::attach(|py| -> Result<Py<PyAny>> {
            let raw = self.0.call_method0(py, "iter_files_recursive")?;
            let it = raw.bind(py).try_iter().map_err(Error::from)?;
            Ok(it.unbind().into_any())
        });

        let iter = match iter {
            Ok(i) => i,
            Err(e) => return Box::new(std::iter::once(Err(e))),
        };

        Box::new(std::iter::from_fn(move || {
            Python::attach(|py| -> Option<Result<String>> {
                match iter.call_method0(py, "__next__") {
                    Ok(obj) => {
                        if obj.is_none(py) {
                            None
                        } else {
                            Some(obj.extract::<String>(py).map_err(Error::from))
                        }
                    }
                    Err(e) if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) => None,
                    Err(e) => Some(Err(Error::from(e))),
                }
            })
        }))
    }

    fn open_write_stream(
        &self,
        relpath: &UrlFragment,
        permissions: Option<Permissions>,
    ) -> Result<Box<dyn WriteStream + Send + Sync>> {
        Python::attach(|py| {
            let obj = self.0.call_method1(
                py,
                "open_write_stream",
                (relpath, perms_to_py_mode(permissions.as_ref())),
            )?;
            let file = PyWriteStream(obj);
            Ok(Box::new(file) as Box<dyn WriteStream + Send + Sync>)
        })
    }

    fn delete_tree(&self, relpath: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "delete_tree", (relpath,))?;
            Ok(())
        })
    }

    fn r#move(&self, src: &UrlFragment, dst: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "move", (src, dst))?;
            Ok(())
        })
    }

    fn copy_tree(&self, src: &UrlFragment, dst: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "copy_tree", (src, dst))?;
            Ok(())
        })
    }

    fn copy_tree_to_transport(&self, _to_transport: &dyn Transport) -> Result<()> {
        // TODO(jelmer): bridge copy_tree_to_transport across the Py↔Rust
        // boundary. The Python side expects a Transport object, but we'd
        // need to obtain its underlying Py wrapper. For now signal the
        // caller that this transport can't perform the operation.
        Err(Error::TransportNotPossible)
    }

    fn copy_to(
        &self,
        _relpaths: &[&UrlFragment],
        _to_transport: &dyn Transport,
        _permissions: Option<Permissions>,
    ) -> Result<usize> {
        // TODO(jelmer): same blocker as copy_tree_to_transport — the
        // destination transport isn't readily expressible as a Py object
        // from inside the Rust adapter.
        Err(Error::TransportNotPossible)
    }

    fn can_roundtrip_unix_modebits(&self) -> bool {
        // Python convention names this `_can_roundtrip_unix_modebits` (with
        // a leading underscore) on every transport class; the unprefixed
        // form does not exist on the Python side. Default to false if the
        // wrapped object doesn't expose either spelling.
        Python::attach(|py| {
            self.0
                .call_method0(py, "_can_roundtrip_unix_modebits")
                .ok()
                .and_then(|obj| obj.extract::<bool>(py).ok())
                .unwrap_or(false)
        })
    }

    fn local_abspath(&self, relpath: &UrlFragment) -> Result<PathBuf> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "local_abspath", (relpath,))?;
            Ok(obj.extract::<PathBuf>(py)?)
        })
    }

    fn list_dir(&self, relpath: &UrlFragment) -> Box<dyn Iterator<Item = Result<String>>> {
        // Python list_dir may return a list, tuple, or iterator. Coerce via
        // try_iter so __next__ always works.
        let iter = Python::attach(|py| -> Result<Py<PyAny>> {
            let raw = self.0.call_method1(py, "list_dir", (relpath,))?;
            let it = raw.bind(py).try_iter().map_err(Error::from)?;
            Ok(it.unbind().into_any())
        });

        let iter = match iter {
            Ok(i) => i,
            Err(e) => return Box::new(std::iter::once(Err(e))),
        };

        Box::new(std::iter::from_fn(move || {
            Python::attach(|py| -> Option<Result<String>> {
                match iter.call_method0(py, "__next__") {
                    Ok(obj) => {
                        if obj.is_none(py) {
                            None
                        } else {
                            Some(obj.extract::<String>(py).map_err(|e| e.into()))
                        }
                    }
                    Err(e) if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) => None,
                    Err(e) => Some(Err(e.into())),
                }
            })
        }))
    }

    fn listable(&self) -> bool {
        Python::attach(|py| {
            self.0
                .call_method0(py, "listable")
                .ok()
                .and_then(|obj| obj.extract::<bool>(py).ok())
                .unwrap_or(false)
        })
    }

    fn lock_write(&self, relpath: &UrlFragment) -> Result<Box<dyn Lock + Send + Sync>> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "lock_write", (relpath,))?;
            let file: Box<dyn Lock + Send + Sync> = Box::new(PyLock(obj));
            Ok(file)
        })
    }

    fn lock_read(&self, relpath: &UrlFragment) -> Result<Box<dyn Lock + Send + Sync>> {
        Python::attach(|py| {
            let obj = self.0.call_method1(py, "lock_read", (relpath,))?;
            let file: Box<dyn Lock + Send + Sync> = Box::new(PyLock(obj));
            Ok(file)
        })
    }

    fn get_smart_medium(&self) -> Result<Box<dyn SmartMedium>> {
        Python::attach(|py| {
            let obj = match self.0.call_method0(py, "get_smart_medium") {
                Ok(o) => o,
                Err(e) if e.is_instance_of::<NoSmartMedium>(py) => {
                    return Err(Error::NoSmartMedium);
                }
                Err(e) => return Err(Error::Io(std::io::Error::other(e.to_string()))),
            };
            if obj.is_none(py) {
                return Err(Error::NoSmartMedium);
            }
            let medium = PySmartMedium(obj);
            Ok(Box::new(medium) as Box<dyn SmartMedium>)
        })
    }

    fn copy(&self, src: &UrlFragment, dst: &UrlFragment) -> Result<()> {
        Python::attach(|py| {
            self.0.call_method1(py, "copy", (src, dst))?;
            Ok(())
        })
    }
}
