// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

use pyo3::IntoPyObjectExt;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use pyo3::types::PyDict;
use pyo3::types::PyTuple;
use pyo3::types::PyType;
use pyo3_async_runtimes::tokio::future_into_py;

use crate::*;

fn build_operator(scheme: &str, map: HashMap<String, String>) -> PyResult<ocore::Operator> {
    let op = ocore::Operator::via_iter(scheme, map).map_err(format_pyerr)?;
    Ok(op)
}

fn build_blocking_operator(
    scheme: &str,
    map: HashMap<String, String>,
) -> PyResult<ocore::blocking::Operator> {
    let op = ocore::Operator::via_iter(scheme, map).map_err(format_pyerr)?;

    let runtime = pyo3_async_runtimes::tokio::get_runtime();
    let _guard = runtime.enter();
    let op = ocore::blocking::Operator::new(op).map_err(format_pyerr)?;
    Ok(op)
}

fn build_operator_from_uri(uri: &str, map: HashMap<String, String>) -> PyResult<ocore::Operator> {
    let op = ocore::Operator::from_uri((uri, map)).map_err(format_pyerr)?;
    Ok(op)
}

fn build_blocking_operator_from_uri(
    uri: &str,
    map: HashMap<String, String>,
) -> PyResult<ocore::blocking::Operator> {
    let op = build_operator_from_uri(uri, map)?;

    let runtime = pyo3_async_runtimes::tokio::get_runtime();
    let _guard = runtime.enter();
    let op = ocore::blocking::Operator::new(op).map_err(format_pyerr)?;
    Ok(op)
}

fn normalize_scheme(raw: &str) -> String {
    raw.trim().to_ascii_lowercase().replace('_', "-")
}

/// Convert a config value into the string form core's config deserializer
/// consumes.
///
/// Accepts the native types the `opendal.config` TypedDicts declare: `str`,
/// `bool`, `int`, `os.PathLike`, and `list`/`tuple` of those (`,`-joined, as
/// core parses `Vec`). A nested `dict` has no flat-map form and is rejected.
fn config_value_to_string(value: &Bound<PyAny>) -> PyResult<String> {
    // `str` before the list branch (a `str` is also a sequence); `bool` before
    // `int` (a Python `bool` also extracts as `int`); `dict` before the list
    // branch so a map is rejected rather than read as its keys.
    if let Ok(s) = value.extract::<String>() {
        Ok(s)
    } else if let Ok(b) = value.extract::<bool>() {
        Ok(if b { "true" } else { "false" }.to_string())
    } else if let Ok(i) = value.extract::<i128>() {
        Ok(i.to_string())
    } else if value.cast::<PyDict>().is_ok() {
        Err(Unsupported::new_err(
            "a map-valued config field cannot be built via from_config; leave it unset",
        ))
    } else if let Ok(items) = value.extract::<Vec<Bound<PyAny>>>() {
        let parts = items
            .iter()
            .map(config_value_to_string)
            .collect::<PyResult<Vec<_>>>()?;
        Ok(parts.join(","))
    } else if let Ok(path) = value.extract::<PathBuf>() {
        Ok(path.to_string_lossy().into_owned())
    } else {
        Err(Unsupported::new_err(
            "unsupported config value type; pass a str, bool, int, os.PathLike, or list of those",
        ))
    }
}

/// Extract `(scheme, config_map)` from a typed service config dict.
///
/// A config is a plain `dict` (an `opendal.config.ServiceConfig`, e.g.
/// `S3Config`) whose `scheme` key selects the service; every other pair becomes
/// a config option, converted via [`config_value_to_string`].
fn extract_typed_config(config: &Bound<PyAny>) -> PyResult<(String, HashMap<String, String>)> {
    let dict = config.cast::<PyDict>().map_err(|_| {
        Unsupported::new_err(
            "from_config expects an opendal.config.ServiceConfig \
             (a dict with a 'scheme' key, e.g. S3Config(scheme=\"s3\", ...))",
        )
    })?;

    let scheme = dict
        .get_item("scheme")?
        .ok_or_else(|| Unsupported::new_err("config is missing the required 'scheme' key"))?
        .extract::<String>()?;

    let mut map = HashMap::with_capacity(dict.len());
    for (k, v) in dict.iter() {
        let key = k.extract::<String>()?;
        if key != "scheme" {
            let value = config_value_to_string(&v)?;
            map.insert(key, value);
        }
    }

    Ok((scheme, map))
}

/// Rebuild a blocking [`Operator`] while unpickling.
///
/// Routes through `from_uri`, not the scheme-based `__new__`, whose scheme
/// normalization would corrupt a URI held in `__scheme`. Bare schemes work too:
/// the core resolves both through the same path.
#[pyfunction]
pub fn _reconstruct_operator(scheme: &str, map: HashMap<String, String>) -> PyResult<Operator> {
    Ok(Operator {
        core: build_blocking_operator_from_uri(scheme, map.clone())?,
        __scheme: scheme.to_string(),
        __map: map,
    })
}

/// Rebuild an [`AsyncOperator`] while unpickling.
///
/// See [`_reconstruct_operator`] for why a dedicated reconstructor is used.
#[pyfunction]
pub fn _reconstruct_async_operator(
    scheme: &str,
    map: HashMap<String, String>,
) -> PyResult<AsyncOperator> {
    Ok(AsyncOperator {
        core: build_operator_from_uri(scheme, map.clone())?,
        __scheme: scheme.to_string(),
        __map: map,
    })
}

/// The blocking equivalent of `AsyncOperator`.
///
/// `Operator` is the entry point for all blocking APIs.
///
/// See also
/// --------
/// AsyncOperator
#[pyclass(module = "opendal.operator")]
pub struct Operator {
    core: ocore::blocking::Operator,
    __scheme: String,
    __map: HashMap<String, String>,
}
#[pymethods]
impl Operator {
    /// Create a new blocking `Operator`.
    ///
    /// Parameters
    /// ----------
    /// scheme : str | Scheme
    ///     The scheme of the service.
    /// **kwargs : dict
    ///     The options for the service.
    ///
    /// Returns
    /// -------
    /// Operator
    ///     The new operator.
    #[new]
    #[pyo3(signature = (scheme: "str | Scheme", *, **kwargs: "str"))]
    pub fn new(scheme: Bound<PyAny>, kwargs: Option<HashMap<String, String>>) -> PyResult<Self> {
        let scheme = if let Ok(scheme_str) = scheme.extract::<&str>() {
            scheme_str.to_string()
        } else if let Ok(py_scheme) = scheme.extract::<Scheme>() {
            String::from(py_scheme)
        } else {
            return Err(Unsupported::new_err(
                "Invalid type for scheme, expected str or Scheme",
            ));
        };
        let scheme = normalize_scheme(&scheme);
        let map = kwargs.unwrap_or_default();

        Ok(Operator {
            core: build_blocking_operator(&scheme, map.clone())?,
            __scheme: scheme,
            __map: map,
        })
    }

    /// Create a new blocking `Operator` from a URI string.
    ///
    /// The URI encodes the scheme and configuration in a single string, e.g.
    /// ``memory://`` or ``s3://bucket/path?region=us-east-1``. The scheme must
    /// belong to a service enabled in this build. Encode service options as
    /// query parameters; use ``urllib.parse.urlencode`` when building the URI
    /// dynamically.
    ///
    /// Parameters
    /// ----------
    /// uri : str
    ///     The URI of the service, including any options as query parameters.
    /// **kwargs : dict
    ///     Overrides for URI options. Prefer the URI query string.
    ///
    /// Returns
    /// -------
    /// Operator
    ///     The new operator.
    ///
    /// Examples
    /// --------
    /// ```python
    /// from urllib.parse import urlencode
    /// import opendal
    ///
    /// op = opendal.Operator.from_uri("memory://")
    /// query = urlencode({"region": "us-east-1"})
    /// op = opendal.Operator.from_uri(f"s3://bucket/path?{query}")
    /// ```
    #[classmethod]
    #[pyo3(signature = (uri, **kwargs: "str"))]
    pub fn from_uri(
        _cls: &Bound<PyType>,
        uri: &str,
        kwargs: Option<HashMap<String, String>>,
    ) -> PyResult<Self> {
        let map = kwargs.unwrap_or_default();

        Ok(Operator {
            core: build_blocking_operator_from_uri(uri, map.clone())?,
            __scheme: uri.to_string(),
            __map: map,
        })
    }

    /// Create a new blocking `Operator` from a typed service config.
    ///
    /// The config is an ``opendal.config.ServiceConfig`` (e.g. ``S3Config``); its
    /// ``scheme`` key selects the service, so a static type checker rejects a
    /// wrong scheme, a missing required key, an unknown key, and a wrong value
    /// type. Non-string values (``bool``, ``int``, ``os.PathLike``, ``list``)
    /// are converted to the string form core consumes.
    ///
    /// Parameters
    /// ----------
    /// config : ServiceConfig
    ///     A service configuration such as ``opendal.config.S3Config``.
    ///
    /// Returns
    /// -------
    /// Operator
    ///     The new operator.
    ///
    /// Examples
    /// --------
    /// ```python
    /// import opendal
    /// from opendal.config import S3Config
    ///
    /// op = opendal.Operator.from_config(S3Config(scheme="s3", bucket="my-bucket"))
    /// ```
    #[classmethod]
    #[pyo3(signature = (config: "ServiceConfig"))]
    pub fn from_config(_cls: &Bound<PyType>, config: &Bound<PyAny>) -> PyResult<Self> {
        let (scheme, map) = extract_typed_config(config)?;
        let scheme = normalize_scheme(&scheme);

        Ok(Operator {
            core: build_blocking_operator(&scheme, map.clone())?,
            __scheme: scheme,
            __map: map,
        })
    }

    /// Add a new layer to this operator.
    ///
    /// Parameters
    /// ----------
    /// layer : Layer
    ///     The layer to add.
    ///
    /// Returns
    /// -------
    /// Operator
    ///     A new operator with the layer added.
    pub fn layer(&self, layer: &layers::Layer) -> PyResult<Self> {
        let op = layer.0.layer(self.core.clone().into());

        let runtime = pyo3_async_runtimes::tokio::get_runtime();
        let _guard = runtime.enter();
        let op = ocore::blocking::Operator::new(op).map_err(format_pyerr)?;
        Ok(Self {
            core: op,
            __scheme: self.__scheme.clone(),
            __map: self.__map.clone(),
        })
    }

    /// Open a file-like object for the given path.
    ///
    /// The returning file-like object is a context manager.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// mode : str
    ///     The mode to open the file in. Only "rb" and "wb" are supported.
    /// **kwargs
    ///     Additional options for the underlying reader or writer.
    ///
    /// Returns
    /// -------
    /// File
    ///     A file-like object.
    #[pyo3(signature = (path, mode, *, **kwargs: "Unpack[OpenKwargs]"))]
    pub fn open(
        &self,
        path: PathBuf,
        mode: String,
        kwargs: Option<&Bound<PyDict>>,
    ) -> PyResult<File> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();

        let reader_opts = kwargs
            .map(|v| v.extract::<ReadOptions>())
            .transpose()?
            .unwrap_or_default();

        let writer_opts = kwargs
            .map(|v| v.extract::<WriteOptions>())
            .transpose()?
            .unwrap_or_default();

        if mode == "rb" {
            let range = reader_opts.make_range();
            let reader = this
                .reader_options(&path, reader_opts.into())
                .map_err(format_pyerr)?;

            let r = reader
                .into_std_read(range.to_range())
                .map_err(format_pyerr)?;
            Ok(File::new_reader(r))
        } else if mode == "wb" {
            let writer = this
                .writer_options(&path, writer_opts.into())
                .map_err(format_pyerr)?;
            Ok(File::new_writer(writer))
        } else {
            Err(Unsupported::new_err(format!(
                "OpenDAL doesn't support mode: {mode}"
            )))
        }
    }

    /// Read the entire contents of a file at the given path.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// version : str, optional
    ///     The version of the file.
    /// concurrent : int, optional
    ///     The number of concurrent readers.
    /// chunk : int, optional
    ///     The size of each chunk.
    /// gap : int, optional
    ///     The gap between each chunk.
    /// offset : int, optional
    ///     The offset of the file.
    /// prefetch : int, optional
    ///     The number of bytes to prefetch.
    /// size : int, optional
    ///     The size of the file.
    /// if_match : str, optional
    ///     The ETag of the file.
    /// if_none_match : str, optional
    ///     The ETag of the file.
    /// if_modified_since : str, optional
    ///     The last modified time of the file.
    /// if_unmodified_since : str, optional
    ///     The last modified time of the file.
    /// content_type : str, optional
    ///     The content type of the file.
    /// cache_control : str, optional
    ///     The cache control of the file.
    /// content_disposition : str, optional
    ///     The content disposition of the file.
    ///
    /// Returns
    /// -------
    /// bytes
    ///     The contents of the file as bytes.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, *,
        version=None,
        concurrent=None,
        chunk=None,
        gap=None,
        offset=None,
        prefetch=None,
        size=None,
        if_match=None,
        if_none_match=None,
        if_modified_since=None,
        if_unmodified_since=None,
        content_type=None,
        cache_control=None,
        content_disposition=None) -> "bytes")]
    pub fn read<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        version: Option<String>,
        concurrent: Option<usize>,
        chunk: Option<usize>,
        gap: Option<usize>,
        offset: Option<usize>,
        prefetch: Option<usize>,
        size: Option<usize>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_modified_since: Option<jiff::Timestamp>,
        if_unmodified_since: Option<jiff::Timestamp>,
        content_type: Option<String>,
        cache_control: Option<String>,
        content_disposition: Option<String>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let path = path.to_string_lossy().to_string();
        let opts = ReadOptions {
            version,
            concurrent,
            chunk,
            gap,
            offset,
            prefetch,
            size,
            if_match,
            if_none_match,
            if_modified_since,
            if_unmodified_since,
            content_type,
            cache_control,
            content_disposition,
        };
        let buffer = self
            .core
            .read_options(&path, opts.into())
            .map_err(format_pyerr)?
            .to_vec();

        Buffer::new(buffer).into_bytes_ref(py)
    }

    /// Write bytes to a file at the given path.
    ///
    /// This function will create a file if it does not exist, and will
    /// overwrite its contents if it does.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// bs : bytes
    ///     The contents to write to the file.
    /// append : bool, optional
    ///     Whether to append to the file instead of overwriting it.
    /// chunk : int, optional
    ///     The chunk size to use when writing the file.
    /// concurrent : int, optional
    ///     The number of concurrent requests to make when writing the file.
    /// cache_control : str, optional
    ///     The cache control header to set on the file.
    /// content_type : str, optional
    ///     The content type header to set on the file.
    /// content_disposition : str, optional
    ///     The content disposition header to set on the file.
    /// content_encoding : str, optional
    ///     The content encoding header to set on the file.
    /// if_match : str, optional
    ///     The ETag to match when writing the file.
    /// if_none_match : str, optional
    ///     The ETag to not match when writing the file.
    /// if_not_exists : bool, optional
    ///     Whether to fail if the file already exists.
    /// user_metadata : dict, optional
    ///     The user metadata to set on the file.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, bs: "bytes", *,
        append= None,
        chunk = None,
        concurrent = None,
        cache_control = None,
        content_type = None,
        content_disposition = None,
        content_encoding = None,
        if_match = None,
        if_none_match = None,
        if_not_exists = None,
        user_metadata = None))]
    pub fn write(
        &self,
        path: PathBuf,
        bs: Vec<u8>,
        append: Option<bool>,
        chunk: Option<usize>,
        concurrent: Option<usize>,
        cache_control: Option<String>,
        content_type: Option<String>,
        content_disposition: Option<String>,
        content_encoding: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_not_exists: Option<bool>,
        user_metadata: Option<HashMap<String, String>>,
    ) -> PyResult<()> {
        let path = path.to_string_lossy().to_string();
        let opts = WriteOptions {
            append,
            chunk,
            concurrent,
            cache_control,
            content_type,
            content_disposition,
            content_encoding,
            if_match,
            if_none_match,
            if_not_exists,
            user_metadata,
        };

        self.core
            .write_options(&path, bs, opts.into())
            .map(|_| ())
            .map_err(format_pyerr)
    }

    /// Get the metadata of a file at the given path.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// version : str, optional
    ///     The version of the file.
    /// if_match : str, optional
    ///     The ETag of the file.
    /// if_none_match : str, optional
    ///     The ETag of the file.
    /// if_modified_since : datetime, optional
    ///     The last modified time of the file.
    /// if_unmodified_since : datetime, optional
    ///     The last modified time of the file.
    /// content_type : str, optional
    ///     The content type of the file.
    /// cache_control : str, optional
    ///     The cache control of the file.
    /// content_disposition : str, optional
    ///     The content disposition of the file.
    ///
    /// Returns
    /// -------
    /// Metadata
    ///     The metadata of the file.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, *,
        version=None,
        if_match=None,
        if_none_match=None,
        if_modified_since=None,
        if_unmodified_since=None,
        content_type=None,
        cache_control=None,
        content_disposition=None))]
    pub fn stat(
        &self,
        path: PathBuf,
        version: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_modified_since: Option<jiff::Timestamp>,
        if_unmodified_since: Option<jiff::Timestamp>,
        content_type: Option<String>,
        cache_control: Option<String>,
        content_disposition: Option<String>,
    ) -> PyResult<Metadata> {
        let path = path.to_string_lossy().to_string();
        let opts = StatOptions {
            version,
            if_match,
            if_none_match,
            if_modified_since,
            if_unmodified_since,
            content_type,
            cache_control,
            content_disposition,
        };
        self.core
            .stat_options(&path, opts.into())
            .map_err(format_pyerr)
            .map(Metadata::new)
    }

    /// Copy a file from one path to another.
    ///
    /// Parameters
    /// ----------
    /// source : str
    ///     The path to the source file.
    /// target : str
    ///     The path to the target file.
    pub fn copy(&self, source: PathBuf, target: PathBuf) -> PyResult<()> {
        let source = source.to_string_lossy().to_string();
        let target = target.to_string_lossy().to_string();
        self.core
            .copy(&source, &target)
            .map(|_| ())
            .map_err(format_pyerr)
    }

    /// Rename (move) a file from one path to another.
    ///
    /// Parameters
    /// ----------
    /// source : str
    ///     The path to the source file.
    /// target : str
    ///     The path to the target file.
    pub fn rename(&self, source: PathBuf, target: PathBuf) -> PyResult<()> {
        let source = source.to_string_lossy().to_string();
        let target = target.to_string_lossy().to_string();
        self.core.rename(&source, &target).map_err(format_pyerr)
    }

    /// Recursively remove all files and directories at the given path.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to remove.
    pub fn remove_all(&self, path: PathBuf) -> PyResult<()> {
        use ocore::options::DeleteOptions;
        let path = path.to_string_lossy().to_string();
        self.core
            .delete_options(
                &path,
                DeleteOptions {
                    recursive: true,
                    ..Default::default()
                },
            )
            .map_err(format_pyerr)
    }

    /// Create a directory at the given path.
    ///
    /// Notes
    /// -----
    /// To indicate that a path is a directory, it must end with a `/`.
    /// This operation is always recursive, like `mkdir -p`.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the directory.
    pub fn create_dir(&self, path: PathBuf) -> PyResult<()> {
        let path = path.to_string_lossy().to_string();
        self.core.create_dir(&path).map_err(format_pyerr)
    }

    /// Delete a file at the given path.
    ///
    /// Notes
    /// -----
    /// This operation will not return an error if the path does not exist.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// version : str, optional
    ///     The version of the file to delete. Only supported on version-aware backends.
    /// recursive : bool, optional
    ///     If True, delete the path recursively.
    ///     Only supported on backends that support recursive delete.
    #[pyo3(signature = (path, *, version=None, recursive=None))]
    pub fn delete(
        &self,
        path: PathBuf,
        version: Option<String>,
        recursive: Option<bool>,
    ) -> PyResult<()> {
        let path = path.to_string_lossy().to_string();
        if version.is_some() || recursive.is_some() {
            let opts = ocore::options::DeleteOptions {
                version,
                recursive: recursive.unwrap_or(false),
            };
            self.core.delete_options(&path, opts).map_err(format_pyerr)
        } else {
            self.core.delete(&path).map_err(format_pyerr)
        }
    }

    /// Check if a path exists.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to check.
    ///
    /// Returns
    /// -------
    /// bool
    ///     True if the path exists, False otherwise.
    pub fn exists(&self, path: PathBuf) -> PyResult<bool> {
        let path = path.to_string_lossy().to_string();
        self.core.exists(&path).map_err(format_pyerr)
    }

    /// List entries in the given directory.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the directory.
    /// limit : int, optional
    ///     The maximum number of entries to return.
    /// start_after : str, optional
    ///     The entry to start after.
    /// recursive : bool, optional
    ///     Whether to list recursively.
    /// versions : bool, optional
    ///     Whether to list versions.
    /// deleted : bool, optional
    ///     Whether to list deleted entries.
    ///
    /// Returns
    /// -------
    /// BlockingLister
    ///     An iterator over the entries in the directory.
    #[pyo3(signature = (path, *,
        limit=None,
        start_after=None,
        recursive=None,
        versions=None,
        deleted=None) -> "collections.abc.Iterable[Entry]")]
    pub fn list(
        &self,
        path: PathBuf,
        limit: Option<usize>,
        start_after: Option<String>,
        recursive: Option<bool>,
        versions: Option<bool>,
        deleted: Option<bool>,
    ) -> PyResult<BlockingLister> {
        let path = path.to_string_lossy().to_string();

        let opts = ListOptions {
            limit,
            start_after,
            recursive,
            versions,
            deleted,
        };

        let l = self
            .core
            .lister_options(&path, opts.into())
            .map_err(format_pyerr)?;
        Ok(BlockingLister::new(l))
    }

    /// Recursively list entries in the given directory.
    ///
    /// Deprecated
    /// ----------
    ///     Use `list()` with `recursive=True` instead.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the directory.
    /// limit : int, optional
    ///     The maximum number of entries to return.
    /// start_after : str, optional
    ///     The entry to start after.
    /// versions : bool, optional
    ///     Whether to list versions.
    /// deleted : bool, optional
    ///     Whether to list deleted entries.
    ///
    /// Returns
    /// -------
    /// BlockingLister
    ///     An iterator over the entries in the directory.
    #[pyo3(signature = (path, *,
        limit=None,
        start_after=None,
        versions=None,
        deleted=None) -> "collections.abc.Iterable[Entry]")]
    pub fn scan(
        &self,
        path: PathBuf,
        limit: Option<usize>,
        start_after: Option<String>,
        versions: Option<bool>,
        deleted: Option<bool>,
    ) -> PyResult<BlockingLister> {
        self.list(path, limit, start_after, Some(true), versions, deleted)
    }

    /// Get all capabilities of this operator.
    ///
    /// Returns
    /// -------
    /// Capability
    ///     The capability of the operator.
    pub fn capability(&self) -> PyResult<capability::Capability> {
        Ok(capability::Capability::new(self.core.info().capability()))
    }

    /// Check if the operator is able to work correctly.
    ///
    /// Raises
    /// ------
    /// Exception
    ///     If the operator is not able to work correctly.
    pub fn check(&self) -> PyResult<()> {
        self.core.check().map_err(format_pyerr)
    }

    /// Create a new `AsyncOperator` from this blocking operator.
    ///
    /// Returns
    /// -------
    /// AsyncOperator
    ///     The async operator.
    pub fn to_async_operator(&self) -> PyResult<AsyncOperator> {
        Ok(AsyncOperator {
            core: self.core.clone().into(),
            __scheme: self.__scheme.clone(),
            __map: self.__map.clone(),
        })
    }

    fn __repr__(&self) -> String {
        let info = self.core.info();
        let name = info.name();
        if name.is_empty() {
            format!("Operator(\"{}\", root=\"{}\")", info.scheme(), info.root())
        } else {
            format!(
                "Operator(\"{}\", root=\"{}\", name=\"{name}\")",
                info.scheme(),
                info.root()
            )
        }
    }
    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
        let reconstructor = py
            .import("opendal._opendal")?
            .getattr("_reconstruct_operator")?;
        let args = (self.__scheme.clone(), self.__map.clone()).into_py_any(py)?;
        PyTuple::new(py, [reconstructor.into_py_any(py)?, args])?.into_py_any(py)
    }
}

/// The async equivalent of `Operator`.
///
/// `AsyncOperator` is the entry point for all async APIs.
///
/// See also
/// --------
/// Operator
#[pyclass(module = "opendal.operator")]
pub struct AsyncOperator {
    core: ocore::Operator,
    __scheme: String,
    __map: HashMap<String, String>,
}
#[pymethods]
impl AsyncOperator {
    /// Create a new `AsyncOperator`.
    ///
    /// Parameters
    /// ----------
    /// scheme : str | Scheme
    ///     The scheme of the service.
    /// **kwargs : dict
    ///     The options for the service.
    ///
    /// Returns
    /// -------
    /// AsyncOperator
    ///     The new async operator.
    #[new]
    #[pyo3(signature = (scheme: "str | Scheme", * ,**kwargs: "str"))]
    pub fn new(scheme: Bound<PyAny>, kwargs: Option<HashMap<String, String>>) -> PyResult<Self> {
        let scheme = if let Ok(scheme_str) = scheme.extract::<&str>() {
            scheme_str.to_string()
        } else if let Ok(py_scheme) = scheme.extract::<Scheme>() {
            String::from(py_scheme)
        } else {
            return Err(Unsupported::new_err(
                "Invalid type for scheme, expected str or Scheme",
            ));
        };
        let scheme = normalize_scheme(&scheme);

        let map = kwargs.unwrap_or_default();

        Ok(AsyncOperator {
            core: build_operator(&scheme, map.clone())?,
            __scheme: scheme,
            __map: map,
        })
    }

    /// Create a new `AsyncOperator` from a URI string.
    ///
    /// The URI encodes the scheme and configuration in a single string, e.g.
    /// ``memory://`` or ``s3://bucket/path?region=us-east-1``. The scheme must
    /// belong to a service enabled in this build. Encode service options as
    /// query parameters; use ``urllib.parse.urlencode`` when building the URI
    /// dynamically.
    ///
    /// Parameters
    /// ----------
    /// uri : str
    ///     The URI of the service, including any options as query parameters.
    /// **kwargs : dict
    ///     Overrides for URI options. Prefer the URI query string.
    ///
    /// Returns
    /// -------
    /// AsyncOperator
    ///     The new async operator.
    ///
    /// Examples
    /// --------
    /// ```python
    /// from urllib.parse import urlencode
    /// import opendal
    ///
    /// op = opendal.AsyncOperator.from_uri("memory://")
    /// query = urlencode({"region": "us-east-1"})
    /// op = opendal.AsyncOperator.from_uri(f"s3://bucket/path?{query}")
    /// ```
    #[classmethod]
    #[pyo3(signature = (uri, **kwargs: "str"))]
    pub fn from_uri(
        _cls: &Bound<PyType>,
        uri: &str,
        kwargs: Option<HashMap<String, String>>,
    ) -> PyResult<Self> {
        let map = kwargs.unwrap_or_default();

        Ok(AsyncOperator {
            core: build_operator_from_uri(uri, map.clone())?,
            __scheme: uri.to_string(),
            __map: map,
        })
    }

    /// Create a new `AsyncOperator` from a typed service config.
    ///
    /// The config is an ``opendal.config.ServiceConfig`` (e.g. ``S3Config``); its
    /// ``scheme`` key selects the service, so a static type checker rejects a
    /// wrong scheme, a missing required key, an unknown key, and a wrong value
    /// type. Non-string values (``bool``, ``int``, ``os.PathLike``, ``list``)
    /// are converted to the string form core consumes.
    ///
    /// Parameters
    /// ----------
    /// config : ServiceConfig
    ///     A service configuration such as ``opendal.config.S3Config``.
    ///
    /// Returns
    /// -------
    /// AsyncOperator
    ///     The new async operator.
    ///
    /// Examples
    /// --------
    /// ```python
    /// import opendal
    /// from opendal.config import S3Config
    ///
    /// op = opendal.AsyncOperator.from_config(S3Config(scheme="s3", bucket="my-bucket"))
    /// ```
    #[classmethod]
    #[pyo3(signature = (config: "ServiceConfig"))]
    pub fn from_config(_cls: &Bound<PyType>, config: &Bound<PyAny>) -> PyResult<Self> {
        let (scheme, map) = extract_typed_config(config)?;
        let scheme = normalize_scheme(&scheme);

        Ok(AsyncOperator {
            core: build_operator(&scheme, map.clone())?,
            __scheme: scheme,
            __map: map,
        })
    }

    /// Add a new layer to the operator.
    ///
    /// Parameters
    /// ----------
    /// layer : Layer
    ///     The layer to add.
    ///
    /// Returns
    /// -------
    /// AsyncOperator
    ///     A new operator with the layer added.
    pub fn layer(&self, layer: &layers::Layer) -> PyResult<Self> {
        let op = layer.0.layer(self.core.clone());
        Ok(Self {
            core: op,
            __scheme: self.__scheme.clone(),
            __map: self.__map.clone(),
        })
    }

    /// Open an async file-like object for the given path.
    ///
    /// The returning async file-like object is a context manager.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// mode : str
    ///     The mode to open the file in. Only "rb" and "wb" are supported.
    /// **kwargs : dict
    ///     Additional options for the underlying reader or writer.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns a file-like object.
    #[pyo3(signature = (path, mode, *, **kwargs: "Unpack[OpenKwargs]") -> "collections.abc.Awaitable[AsyncFile]")]
    pub fn open<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        mode: String,
        kwargs: Option<&Bound<PyDict>>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();

        let reader_opts = kwargs
            .map(|v| v.extract::<ReadOptions>())
            .transpose()?
            .unwrap_or_default();

        let writer_opts = kwargs
            .map(|v| v.extract::<WriteOptions>())
            .transpose()?
            .unwrap_or_default();

        future_into_py(py, async move {
            if mode == "rb" {
                let range = reader_opts.make_range();
                let reader = this
                    .reader_options(&path, reader_opts.into())
                    .await
                    .map_err(format_pyerr)?;

                let r = reader
                    .into_futures_async_read(range.to_range())
                    .await
                    .map_err(format_pyerr)?;
                Ok(AsyncFile::new_reader(r))
            } else if mode == "wb" {
                let writer = this
                    .writer_options(&path, writer_opts.into())
                    .await
                    .map_err(format_pyerr)?;
                let w = writer.into_futures_async_write();
                Ok(AsyncFile::new_writer(w))
            } else {
                Err(Unsupported::new_err(format!(
                    "OpenDAL doesn't support mode: {mode}"
                )))
            }
        })
    }

    /// Read the entire contents of a file at the given path.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// version : str, optional
    ///     The version of the file.
    /// concurrent : int, optional
    ///     The number of concurrent readers.
    /// chunk : int, optional
    ///     The size of each chunk.
    /// gap : int, optional
    ///     The gap between each chunk.
    /// offset : int, optional
    ///     The offset of the file.
    /// prefetch : int, optional
    ///     The number of bytes to prefetch.
    /// size : int, optional
    ///     The size of the file.
    /// if_match : str, optional
    ///     The ETag of the file.
    /// if_none_match : str, optional
    ///     The ETag of the file.
    /// if_modified_since : str, optional
    ///     The last modified time of the file.
    /// if_unmodified_since : str, optional
    ///     The last modified time of the file.
    /// content_type : str, optional
    ///     The content type of the file.
    /// cache_control : str, optional
    ///     The cache control of the file.
    /// content_disposition : str, optional
    ///     The content disposition of the file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns the contents of the file as bytes.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, *,
        version=None,
        concurrent=None,
        chunk=None,
        gap=None,
        offset=None,
        prefetch=None,
        size=None,
        if_match=None,
        if_none_match=None,
        if_modified_since=None,
        if_unmodified_since=None,
        content_type=None,
        cache_control=None,
        content_disposition=None) -> "collections.abc.Awaitable[bytes]")]
    pub fn read<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        version: Option<String>,
        concurrent: Option<usize>,
        chunk: Option<usize>,
        gap: Option<usize>,
        offset: Option<usize>,
        prefetch: Option<usize>,
        size: Option<usize>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_modified_since: Option<jiff::Timestamp>,
        if_unmodified_since: Option<jiff::Timestamp>,
        content_type: Option<String>,
        cache_control: Option<String>,
        content_disposition: Option<String>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = ReadOptions {
            version,
            concurrent,
            chunk,
            gap,
            offset,
            prefetch,
            size,
            if_match,
            if_none_match,
            if_modified_since,
            if_unmodified_since,
            content_type,
            cache_control,
            content_disposition,
        };
        future_into_py(py, async move {
            let range = opts.make_range();
            let res = this
                .reader_options(&path, opts.into())
                .await
                .map_err(format_pyerr)?
                .read(range.to_range())
                .await
                .map_err(format_pyerr)?
                .to_vec();
            Python::attach(|py| Buffer::new(res).into_bytes(py))
        })
    }

    /// Write bytes to a file at the given path.
    ///
    /// This function will create a file if it does not exist, and will
    /// overwrite its contents if it does.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// bs : bytes
    ///     The contents to write to the file.
    /// append : bool, optional
    ///     Whether to append to the file instead of overwriting it.
    /// chunk : int, optional
    ///     The chunk size to use when writing the file.
    /// concurrent : int, optional
    ///     The number of concurrent requests to make when writing the file.
    /// cache_control : str, optional
    ///     The cache control header to set on the file.
    /// content_type : str, optional
    ///     The content type header to set on the file.
    /// content_disposition : str, optional
    ///     The content disposition header to set on the file.
    /// content_encoding : str, optional
    ///     The content encoding header to set on the file.
    /// if_match : str, optional
    ///     The ETag to match when writing the file.
    /// if_none_match : str, optional
    ///     The ETag to not match when writing the file.
    /// if_not_exists : bool, optional
    ///     Whether to fail if the file already exists.
    /// user_metadata : dict, optional
    ///     The user metadata to set on the file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the write is finished.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, bs: "bytes", *,
        append= None,
        chunk = None,
        concurrent = None,
        cache_control = None,
        content_type = None,
        content_disposition = None,
        content_encoding = None,
        if_match = None,
        if_none_match = None,
        if_not_exists = None,
        user_metadata = None) -> "collections.abc.Awaitable[None]")]
    pub fn write<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        bs: &Bound<PyBytes>,
        append: Option<bool>,
        chunk: Option<usize>,
        concurrent: Option<usize>,
        cache_control: Option<String>,
        content_type: Option<String>,
        content_disposition: Option<String>,
        content_encoding: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_not_exists: Option<bool>,
        user_metadata: Option<HashMap<String, String>>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let opts = WriteOptions {
            append,
            chunk,
            concurrent,
            cache_control,
            content_type,
            content_disposition,
            content_encoding,
            if_match,
            if_none_match,
            if_not_exists,
            user_metadata,
        };
        let this = self.core.clone();
        let bs = bs.as_bytes().to_vec();
        let path = path.to_string_lossy().to_string();
        future_into_py(py, async move {
            this.write_options(&path, bs, opts.into())
                .await
                .map(|_| ())
                .map_err(format_pyerr)
        })
    }

    /// Get the metadata of a file at the given path.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    /// version : str, optional
    ///     The version of the file.
    /// if_match : str, optional
    ///     The ETag of the file.
    /// if_none_match : str, optional
    ///     The ETag of the file.
    /// if_modified_since : datetime, optional
    ///     The last modified time of the file.
    /// if_unmodified_since : datetime, optional
    ///     The last modified time of the file.
    /// content_type : str, optional
    ///     The content type of the file.
    /// cache_control : str, optional
    ///     The cache control of the file.
    /// content_disposition : str, optional
    ///     The content disposition of the file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns the metadata of the file.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, *,
        version=None,
        if_match=None,
        if_none_match=None,
        if_modified_since=None,
        if_unmodified_since=None,
        content_type=None,
        cache_control=None,
        content_disposition=None) -> "collections.abc.Awaitable[Metadata]")]
    pub fn stat<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        version: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_modified_since: Option<jiff::Timestamp>,
        if_unmodified_since: Option<jiff::Timestamp>,
        content_type: Option<String>,
        cache_control: Option<String>,
        content_disposition: Option<String>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = StatOptions {
            version,
            if_match,
            if_none_match,
            if_modified_since,
            if_unmodified_since,
            content_type,
            cache_control,
            content_disposition,
        };

        future_into_py(py, async move {
            let res: Metadata = this
                .stat_options(&path, opts.into())
                .await
                .map_err(format_pyerr)
                .map(Metadata::new)?;

            Ok(res)
        })
    }

    /// Copy a file from one path to another.
    ///
    /// Parameters
    /// ----------
    /// source : str
    ///     The path to the source file.
    /// target : str
    ///     The path to the target file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the copy is finished.
    #[pyo3(signature = (source, target) -> "collections.abc.Awaitable[None]")]
    pub fn copy<'p>(
        &'p self,
        py: Python<'p>,
        source: PathBuf,
        target: PathBuf,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let source = source.to_string_lossy().to_string();
        let target = target.to_string_lossy().to_string();
        future_into_py(py, async move {
            this.copy(&source, &target)
                .await
                .map(|_| ())
                .map_err(format_pyerr)
        })
    }

    /// Rename (move) a file from one path to another.
    ///
    /// Parameters
    /// ----------
    /// source : str
    ///     The path to the source file.
    /// target : str
    ///     The path to the target file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the rename is finished.
    #[pyo3(signature = (source, target) -> "collections.abc.Awaitable[None]")]
    pub fn rename<'p>(
        &'p self,
        py: Python<'p>,
        source: PathBuf,
        target: PathBuf,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let source = source.to_string_lossy().to_string();
        let target = target.to_string_lossy().to_string();
        future_into_py(py, async move {
            this.rename(&source, &target).await.map_err(format_pyerr)
        })
    }

    /// Recursively remove all files and directories at the given path.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to remove.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the removal is finished.
    #[pyo3(signature = (path) -> "collections.abc.Awaitable[None]")]
    pub fn remove_all<'p>(&'p self, py: Python<'p>, path: PathBuf) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        future_into_py(py, async move {
            this.delete_with(&path)
                .recursive(true)
                .await
                .map_err(format_pyerr)
        })
    }

    /// Check if the operator is able to work correctly.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the check is finished.
    ///
    /// Raises
    /// ------
    /// Exception
    ///     If the operator is not able to work correctly.
    #[pyo3(signature = () -> "collections.abc.Awaitable[None]")]
    pub fn check<'p>(&'p self, py: Python<'p>) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        future_into_py(py, async move { this.check().await.map_err(format_pyerr) })
    }

    /// Create a directory at the given path.
    ///
    /// Notes
    /// -----
    /// To indicate that a path is a directory, it must end with a `/`.
    /// This operation is always recursive, like `mkdir -p`.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the directory.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the directory is created.
    #[pyo3(signature = (path) -> "collections.abc.Awaitable[None]")]
    pub fn create_dir<'p>(&'p self, py: Python<'p>, path: PathBuf) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        future_into_py(py, async move {
            this.create_dir(&path).await.map_err(format_pyerr)
        })
    }

    /// Delete a file at the given path.
    ///
    /// Notes
    /// -----
    /// This operation will not return an error if the path does not exist.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that completes when the file is deleted.
    /// version : str, optional
    ///     The version of the file to delete. Only supported on version-aware backends.
    /// recursive : bool, optional
    ///     If True, delete the path recursively.
    ///     Only supported on backends that support recursive delete.
    #[pyo3(signature = (path, *, version=None, recursive=None) -> "collections.abc.Awaitable[None]")]
    pub fn delete<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        version: Option<String>,
        recursive: Option<bool>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        future_into_py(py, async move {
            if version.is_some() || recursive.is_some() {
                let opts = ocore::options::DeleteOptions {
                    version,
                    recursive: recursive.unwrap_or(false),
                };
                this.delete_options(&path, opts).await.map_err(format_pyerr)
            } else {
                this.delete(&path).await.map_err(format_pyerr)
            }
        })
    }

    /// Check if a path exists.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to check.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns True if the path exists, False otherwise.
    #[pyo3(signature = (path) -> "collections.abc.Awaitable[bool]")]
    pub fn exists<'p>(&'p self, py: Python<'p>, path: PathBuf) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        future_into_py(
            py,
            async move { this.exists(&path).await.map_err(format_pyerr) },
        )
    }

    /// List entries in the given directory.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the directory.
    /// limit : int, optional
    ///     The maximum number of entries to return.
    /// start_after : str, optional
    ///     The entry to start after.
    /// recursive : bool, optional
    ///     Whether to list recursively.
    /// versions : bool, optional
    ///     Whether to list versions.
    /// deleted : bool, optional
    ///     Whether to list deleted entries.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns an async iterator over the entries.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, *,
        limit=None,
        start_after=None,
        recursive=None,
        versions=None,
        deleted=None) -> "collections.abc.Awaitable[collections.abc.AsyncIterable[Entry]]")]
    pub fn list<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        limit: Option<usize>,
        start_after: Option<String>,
        recursive: Option<bool>,
        versions: Option<bool>,
        deleted: Option<bool>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = ListOptions {
            limit,
            start_after,
            recursive,
            versions,
            deleted,
        };

        future_into_py(py, async move {
            let lister = this
                .lister_options(&path, opts.into())
                .await
                .map_err(format_pyerr)?;
            let pylister = Python::attach(|py| AsyncLister::new(lister).into_py_any(py))?;

            Ok(pylister)
        })
    }

    /// Recursively list entries in the given directory.
    ///
    /// Deprecated
    /// ----------
    ///     Use `list()` with `recursive=True` instead.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path to the directory.
    /// limit : int, optional
    ///     The maximum number of entries to return.
    /// start_after : str, optional
    ///     The entry to start after.
    /// versions : bool, optional
    ///     Whether to list versions.
    /// deleted : bool, optional
    ///     Whether to list deleted entries.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns an async iterator over the entries.
    #[pyo3(signature = (path, *,
        limit=None,
        start_after=None,
        versions=None,
        deleted=None) -> "collections.abc.Awaitable[collections.abc.AsyncIterable[Entry]]")]
    pub fn scan<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        limit: Option<usize>,
        start_after: Option<String>,
        versions: Option<bool>,
        deleted: Option<bool>,
    ) -> PyResult<Bound<'p, PyAny>> {
        self.list(py, path, limit, start_after, Some(true), versions, deleted)
    }

    /// Create a presigned request for a stat operation.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path of the object to stat.
    /// expire_second : int
    ///     The number of seconds until the presigned URL expires.
    /// version : str, optional
    ///     The version of the file.
    /// if_match : str, optional
    ///     The ETag to match.
    /// if_none_match : str, optional
    ///     The ETag to not match.
    /// if_modified_since : datetime, optional
    ///     Only return if modified since this time.
    /// if_unmodified_since : datetime, optional
    ///     Only return if unmodified since this time.
    /// content_type : str, optional
    ///     Override the content type in the presigned response.
    /// cache_control : str, optional
    ///     Override the cache control in the presigned response.
    /// content_disposition : str, optional
    ///     Override the content disposition in the presigned response.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns a presigned request object.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, expire_second, *,
        version=None,
        if_match=None,
        if_none_match=None,
        if_modified_since=None,
        if_unmodified_since=None,
        content_type=None,
        cache_control=None,
        content_disposition=None) -> "collections.abc.Awaitable[PresignedRequest]")]
    pub fn presign_stat<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        expire_second: u64,
        version: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_modified_since: Option<jiff::Timestamp>,
        if_unmodified_since: Option<jiff::Timestamp>,
        content_type: Option<String>,
        cache_control: Option<String>,
        content_disposition: Option<String>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = StatOptions {
            version,
            if_match,
            if_none_match,
            if_modified_since,
            if_unmodified_since,
            content_type,
            cache_control,
            content_disposition,
        };
        future_into_py(py, async move {
            let res = this
                .presign_stat_options(&path, Duration::from_secs(expire_second), opts.into())
                .await
                .map_err(format_pyerr)
                .map(PresignedRequest)?;

            Ok(res)
        })
    }

    /// Create a presigned request for a read operation.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path of the object to read.
    /// expire_second : int
    ///     The number of seconds until the presigned URL expires.
    /// version : str, optional
    ///     The version of the file.
    /// if_match : str, optional
    ///     The ETag to match.
    /// if_none_match : str, optional
    ///     The ETag to not match.
    /// if_modified_since : datetime, optional
    ///     Only return if modified since this time.
    /// if_unmodified_since : datetime, optional
    ///     Only return if unmodified since this time.
    /// content_type : str, optional
    ///     Override the content type in the presigned response.
    /// cache_control : str, optional
    ///     Override the cache control in the presigned response.
    /// content_disposition : str, optional
    ///     Override the content disposition in the presigned response.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns a presigned request object.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, expire_second, *,
        version=None,
        if_match=None,
        if_none_match=None,
        if_modified_since=None,
        if_unmodified_since=None,
        content_type=None,
        cache_control=None,
        content_disposition=None) -> "collections.abc.Awaitable[PresignedRequest]")]
    pub fn presign_read<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        expire_second: u64,
        version: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_modified_since: Option<jiff::Timestamp>,
        if_unmodified_since: Option<jiff::Timestamp>,
        content_type: Option<String>,
        cache_control: Option<String>,
        content_disposition: Option<String>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = ReadOptions {
            version,
            if_match,
            if_none_match,
            if_modified_since,
            if_unmodified_since,
            content_type,
            cache_control,
            content_disposition,
            ..Default::default()
        };
        future_into_py(py, async move {
            let res = this
                .presign_read_options(&path, Duration::from_secs(expire_second), opts.into())
                .await
                .map_err(format_pyerr)
                .map(PresignedRequest)?;

            Ok(res)
        })
    }

    /// Create a presigned request for a write operation.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path of the object to write to.
    /// expire_second : int
    ///     The number of seconds until the presigned URL expires.
    /// content_type : str, optional
    ///     The content type header to set on the file.
    /// content_disposition : str, optional
    ///     The content disposition header to set on the file.
    /// content_encoding : str, optional
    ///     The content encoding header to set on the file.
    /// cache_control : str, optional
    ///     The cache control header to set on the file.
    /// if_match : str, optional
    ///     The ETag to match when writing the file.
    /// if_none_match : str, optional
    ///     The ETag to not match when writing the file.
    /// if_not_exists : bool, optional
    ///     Whether to fail if the file already exists.
    /// user_metadata : dict, optional
    ///     The user metadata to set on the file.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns a presigned request object.
    #[allow(clippy::too_many_arguments)]
    #[pyo3(signature = (path, expire_second, *,
        content_type=None,
        content_disposition=None,
        content_encoding=None,
        cache_control=None,
        if_match=None,
        if_none_match=None,
        if_not_exists=None,
        user_metadata=None) -> "collections.abc.Awaitable[PresignedRequest]")]
    pub fn presign_write<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        expire_second: u64,
        content_type: Option<String>,
        content_disposition: Option<String>,
        content_encoding: Option<String>,
        cache_control: Option<String>,
        if_match: Option<String>,
        if_none_match: Option<String>,
        if_not_exists: Option<bool>,
        user_metadata: Option<HashMap<String, String>>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = WriteOptions {
            content_type,
            content_disposition,
            content_encoding,
            cache_control,
            if_match,
            if_none_match,
            if_not_exists,
            user_metadata,
            ..Default::default()
        };
        future_into_py(py, async move {
            let res = this
                .presign_write_options(&path, Duration::from_secs(expire_second), opts.into())
                .await
                .map_err(format_pyerr)
                .map(PresignedRequest)?;

            Ok(res)
        })
    }

    /// Create a presigned request for a delete operation.
    ///
    /// Parameters
    /// ----------
    /// path : str
    ///     The path of the object to delete.
    /// expire_second : int
    ///     The number of seconds until the presigned URL expires.
    /// version : str, optional
    ///     The version of the file to delete.
    ///
    /// Returns
    /// -------
    /// coroutine
    ///     An awaitable that returns a presigned request object.
    #[pyo3(signature = (path, expire_second, *, version=None) -> "collections.abc.Awaitable[PresignedRequest]")]
    pub fn presign_delete<'p>(
        &'p self,
        py: Python<'p>,
        path: PathBuf,
        expire_second: u64,
        version: Option<String>,
    ) -> PyResult<Bound<'p, PyAny>> {
        let this = self.core.clone();
        let path = path.to_string_lossy().to_string();
        let opts = DeleteOptions {
            version,
            ..Default::default()
        };
        future_into_py(py, async move {
            let res = this
                .presign_delete_options(&path, Duration::from_secs(expire_second), opts.into())
                .await
                .map_err(format_pyerr)
                .map(PresignedRequest)?;

            Ok(res)
        })
    }

    /// Get all capabilities of this operator.
    ///
    /// Returns
    /// -------
    /// Capability
    ///     The capability of the operator.
    pub fn capability(&self) -> PyResult<Capability> {
        Ok(capability::Capability::new(self.core.info().capability()))
    }

    /// Create a new blocking `Operator` from this async operator.
    ///
    /// Returns
    /// -------
    /// Operator
    ///     The blocking operator.
    pub fn to_operator(&self) -> PyResult<Operator> {
        let runtime = pyo3_async_runtimes::tokio::get_runtime();
        let _guard = runtime.enter();
        let op = ocore::blocking::Operator::new(self.core.clone()).map_err(format_pyerr)?;

        Ok(Operator {
            core: op,
            __scheme: self.__scheme.clone(),
            __map: self.__map.clone(),
        })
    }

    fn __repr__(&self) -> String {
        let info = self.core.info();
        let name = info.name();
        if name.is_empty() {
            format!(
                "AsyncOperator(\"{}\", root=\"{}\")",
                info.scheme(),
                info.root()
            )
        } else {
            format!(
                "AsyncOperator(\"{}\", root=\"{}\", name=\"{name}\")",
                info.scheme(),
                info.root()
            )
        }
    }
    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
        let reconstructor = py
            .import("opendal._opendal")?
            .getattr("_reconstruct_async_operator")?;
        let args = (self.__scheme.clone(), self.__map.clone()).into_py_any(py)?;
        PyTuple::new(py, [reconstructor.into_py_any(py)?, args])?.into_py_any(py)
    }
}

/// A presigned request.
///
/// This contains the information required to make a request to the
/// underlying service, including the URL, method, and headers.
#[pyclass(module = "opendal.types")]
pub struct PresignedRequest(ocore::raw::PresignedRequest);
#[pymethods]
impl PresignedRequest {
    /// The URL of this request.
    #[getter]
    pub fn url(&self) -> String {
        self.0.uri().to_string()
    }

    /// The HTTP method of this request.
    #[getter]
    pub fn method(&self) -> &str {
        self.0.method().as_str()
    }

    /// The HTTP headers of this request.
    ///
    /// Returns
    /// -------
    /// dict
    ///     The HTTP headers of this request.
    #[getter]
    pub fn headers(&self) -> PyResult<HashMap<&str, &str>> {
        let mut headers = HashMap::new();
        for (k, v) in self.0.header().iter() {
            let k = k.as_str();
            let v = v
                .to_str()
                .map_err(|err| Unexpected::new_err(err.to_string()))?;
            if headers.insert(k, v).is_some() {
                return Err(Unexpected::new_err("duplicate header"));
            }
        }
        Ok(headers)
    }
}
