"""Aerospike Python Client - PyO3 + Rust binding.

Drop-in compatible replacement for the aerospike-client-python package.
"""

import asyncio
import logging
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any

from aerospike_py._aerospike import Client as _NativeClient
from aerospike_py._aerospike import AsyncClient as _NativeAsyncClient
from aerospike_py._aerospike import Query as _NativeQuery  # noqa: F401
from aerospike_py._aerospike import BatchRecord, BatchRecords  # noqa: F401
from aerospike_py._aerospike import get_metrics_text as _get_metrics_text
from aerospike_py._aerospike import set_metrics_enabled as _set_metrics_enabled
from aerospike_py._aerospike import is_metrics_enabled as _is_metrics_enabled
from aerospike_py._aerospike import init_tracing as _init_tracing
from aerospike_py._aerospike import shutdown_tracing as _shutdown_tracing

# Import all exceptions from native module
from aerospike_py._aerospike import (  # noqa: F401
    AerospikeError,
    ClientError,
    ClusterError,
    InvalidArgError,
    RecordError,
    ServerError,
    AerospikeTimeoutError,
    TimeoutError,  # deprecated alias for AerospikeTimeoutError
    RecordNotFound,
    RecordExistsError,
    RecordGenerationError,
    RecordTooBig,
    BinNameError,
    BinExistsError,
    BinNotFound,
    BinTypeError,
    FilteredOut,
    AerospikeIndexError,
    IndexError,  # deprecated alias for AerospikeIndexError
    IndexNotFound,
    IndexFoundError,
    QueryError,
    QueryAbortedError,
    AdminError,
    UDFError,
)

# Import all constants from native module
from aerospike_py._aerospike import (  # noqa: F401
    # Policy Key
    POLICY_KEY_DIGEST,
    POLICY_KEY_SEND,
    # Policy Exists
    POLICY_EXISTS_IGNORE,
    POLICY_EXISTS_UPDATE,
    POLICY_EXISTS_UPDATE_ONLY,
    POLICY_EXISTS_REPLACE,
    POLICY_EXISTS_REPLACE_ONLY,
    POLICY_EXISTS_CREATE_ONLY,
    # Policy Gen
    POLICY_GEN_IGNORE,
    POLICY_GEN_EQ,
    POLICY_GEN_GT,
    # Policy Replica
    POLICY_REPLICA_MASTER,
    POLICY_REPLICA_SEQUENCE,
    POLICY_REPLICA_PREFER_RACK,
    # Policy Commit Level
    POLICY_COMMIT_LEVEL_ALL,
    POLICY_COMMIT_LEVEL_MASTER,
    # Policy Read Mode AP
    POLICY_READ_MODE_AP_ONE,
    POLICY_READ_MODE_AP_ALL,
    # TTL Constants
    TTL_NAMESPACE_DEFAULT,
    TTL_NEVER_EXPIRE,
    TTL_DONT_UPDATE,
    TTL_CLIENT_DEFAULT,
    # Auth Mode
    AUTH_INTERNAL,
    AUTH_EXTERNAL,
    AUTH_PKI,
    # Operator Constants
    OPERATOR_READ,
    OPERATOR_WRITE,
    OPERATOR_INCR,
    OPERATOR_APPEND,
    OPERATOR_PREPEND,
    OPERATOR_TOUCH,
    OPERATOR_DELETE,
    # Index Type
    INDEX_NUMERIC,
    INDEX_STRING,
    INDEX_BLOB,
    INDEX_GEO2DSPHERE,
    # Index Collection Type
    INDEX_TYPE_DEFAULT,
    INDEX_TYPE_LIST,
    INDEX_TYPE_MAPKEYS,
    INDEX_TYPE_MAPVALUES,
    # Log Level
    LOG_LEVEL_OFF,
    LOG_LEVEL_ERROR,
    LOG_LEVEL_WARN,
    LOG_LEVEL_INFO,
    LOG_LEVEL_DEBUG,
    LOG_LEVEL_TRACE,
    # Serializer
    SERIALIZER_NONE,
    SERIALIZER_PYTHON,
    SERIALIZER_USER,
    # List Return Type
    LIST_RETURN_NONE,
    LIST_RETURN_INDEX,
    LIST_RETURN_REVERSE_INDEX,
    LIST_RETURN_RANK,
    LIST_RETURN_REVERSE_RANK,
    LIST_RETURN_COUNT,
    LIST_RETURN_VALUE,
    LIST_RETURN_EXISTS,
    # List Order
    LIST_UNORDERED,
    LIST_ORDERED,
    # List Sort Flags
    LIST_SORT_DEFAULT,
    LIST_SORT_DROP_DUPLICATES,
    # List Write Flags
    LIST_WRITE_DEFAULT,
    LIST_WRITE_ADD_UNIQUE,
    LIST_WRITE_INSERT_BOUNDED,
    LIST_WRITE_NO_FAIL,
    LIST_WRITE_PARTIAL,
    # Map Return Type
    MAP_RETURN_NONE,
    MAP_RETURN_INDEX,
    MAP_RETURN_REVERSE_INDEX,
    MAP_RETURN_RANK,
    MAP_RETURN_REVERSE_RANK,
    MAP_RETURN_COUNT,
    MAP_RETURN_KEY,
    MAP_RETURN_VALUE,
    MAP_RETURN_KEY_VALUE,
    MAP_RETURN_EXISTS,
    # Map Order
    MAP_UNORDERED,
    MAP_KEY_ORDERED,
    MAP_KEY_VALUE_ORDERED,
    # Map Write Flags
    MAP_WRITE_FLAGS_DEFAULT,
    MAP_WRITE_FLAGS_CREATE_ONLY,
    MAP_WRITE_FLAGS_UPDATE_ONLY,
    MAP_WRITE_FLAGS_NO_FAIL,
    MAP_WRITE_FLAGS_PARTIAL,
    MAP_UPDATE,
    MAP_UPDATE_ONLY,
    MAP_CREATE_ONLY,
    # Bit Write Flags
    BIT_WRITE_DEFAULT,
    BIT_WRITE_CREATE_ONLY,
    BIT_WRITE_UPDATE_ONLY,
    BIT_WRITE_NO_FAIL,
    BIT_WRITE_PARTIAL,
    # HLL Write Flags
    HLL_WRITE_DEFAULT,
    HLL_WRITE_CREATE_ONLY,
    HLL_WRITE_UPDATE_ONLY,
    HLL_WRITE_NO_FAIL,
    HLL_WRITE_ALLOW_FOLD,
    # Privilege codes
    PRIV_READ,
    PRIV_WRITE,
    PRIV_READ_WRITE,
    PRIV_READ_WRITE_UDF,
    PRIV_SYS_ADMIN,
    PRIV_USER_ADMIN,
    PRIV_DATA_ADMIN,
    PRIV_UDF_ADMIN,
    PRIV_SINDEX_ADMIN,
    PRIV_TRUNCATE,
    # Status codes
    AEROSPIKE_OK,
    AEROSPIKE_ERR_SERVER,
    AEROSPIKE_ERR_RECORD_NOT_FOUND,
    AEROSPIKE_ERR_RECORD_GENERATION,
    AEROSPIKE_ERR_PARAM,
    AEROSPIKE_ERR_RECORD_EXISTS,
    AEROSPIKE_ERR_BIN_EXISTS,
    AEROSPIKE_ERR_CLUSTER_KEY_MISMATCH,
    AEROSPIKE_ERR_SERVER_MEM,
    AEROSPIKE_ERR_TIMEOUT,
    AEROSPIKE_ERR_ALWAYS_FORBIDDEN,
    AEROSPIKE_ERR_PARTITION_UNAVAILABLE,
    AEROSPIKE_ERR_BIN_TYPE,
    AEROSPIKE_ERR_RECORD_TOO_BIG,
    AEROSPIKE_ERR_KEY_BUSY,
    AEROSPIKE_ERR_SCAN_ABORT,
    AEROSPIKE_ERR_UNSUPPORTED_FEATURE,
    AEROSPIKE_ERR_BIN_NOT_FOUND,
    AEROSPIKE_ERR_DEVICE_OVERLOAD,
    AEROSPIKE_ERR_KEY_MISMATCH,
    AEROSPIKE_ERR_INVALID_NAMESPACE,
    AEROSPIKE_ERR_BIN_NAME,
    AEROSPIKE_ERR_FAIL_FORBIDDEN,
    AEROSPIKE_ERR_ELEMENT_NOT_FOUND,
    AEROSPIKE_ERR_ELEMENT_EXISTS,
    AEROSPIKE_ERR_ENTERPRISE_ONLY,
    AEROSPIKE_ERR_OP_NOT_APPLICABLE,
    AEROSPIKE_ERR_FILTERED_OUT,
    AEROSPIKE_ERR_LOST_CONFLICT,
    AEROSPIKE_QUERY_END,
    AEROSPIKE_SECURITY_NOT_SUPPORTED,
    AEROSPIKE_SECURITY_NOT_ENABLED,
    AEROSPIKE_ERR_INVALID_USER,
    AEROSPIKE_ERR_NOT_AUTHENTICATED,
    AEROSPIKE_ERR_ROLE_VIOLATION,
    AEROSPIKE_ERR_UDF,
    AEROSPIKE_ERR_BATCH_DISABLED,
    AEROSPIKE_ERR_INDEX_FOUND,
    AEROSPIKE_ERR_INDEX_NOT_FOUND,
    AEROSPIKE_ERR_QUERY_ABORTED,
    AEROSPIKE_ERR_CLIENT,
    AEROSPIKE_ERR_CONNECTION,
    AEROSPIKE_ERR_CLUSTER,
    AEROSPIKE_ERR_INVALID_HOST,
    AEROSPIKE_ERR_NO_MORE_CONNECTIONS,
)

# Re-export exception subclasses from exception module for backward compat
from aerospike_py import exception  # noqa: F401
from aerospike_py import predicates  # noqa: F401
from aerospike_py.numpy_batch import NumpyBatchRecords  # noqa: F401
from aerospike_py import list_operations  # noqa: F401
from aerospike_py import map_operations  # noqa: F401
from aerospike_py import exp  # noqa: F401
from aerospike_py.types import (  # noqa: F401
    AerospikeKey,
    RecordMetadata,
    Record,
    ExistsResult,
    InfoNodeResult,
    BinTuple,
    OperateOrderedResult,
    Bins,
    ReadPolicy,
    WritePolicy,
    BatchPolicy,
    AdminPolicy,
    QueryPolicy,
    WriteMeta,
    ClientConfig,
    Privilege,
    UserInfo,
    RoleInfo,
)
from aerospike_py._types import ListPolicy, MapPolicy, Operation  # noqa: F401
from aerospike_py._bug_report import catch_unexpected

try:
    from importlib.metadata import PackageNotFoundError
    from importlib.metadata import version as _get_version

    __version__ = _get_version("aerospike-py")
except PackageNotFoundError:
    __version__ = "0.0.0"  # Fallback for development

logger = logging.getLogger("aerospike_py")
logger.addHandler(logging.NullHandler())


# ---------------------------------------------------------------------------
# Wrapping helpers
# ---------------------------------------------------------------------------


def _wrap_key(raw: tuple | None) -> AerospikeKey | None:
    if raw is None:
        return None
    return AerospikeKey(raw[0], raw[1], raw[2], raw[3])


def _wrap_meta(raw: dict | None) -> RecordMetadata | None:
    if raw is None:
        return None
    return RecordMetadata(gen=raw["gen"], ttl=raw["ttl"])


def _wrap_record(raw: tuple) -> Record:
    return Record(key=_wrap_key(raw[0]), meta=_wrap_meta(raw[1]), bins=raw[2])


def _wrap_exists(raw: tuple) -> ExistsResult:
    return ExistsResult(key=_wrap_key(raw[0]), meta=_wrap_meta(raw[1]))


def _wrap_operate_ordered(raw: tuple) -> OperateOrderedResult:
    return OperateOrderedResult(
        key=_wrap_key(raw[0]),
        meta=_wrap_meta(raw[1]),
        ordered_bins=[BinTuple(n, v) for n, v in raw[2]],
    )


# ---------------------------------------------------------------------------
# Query Python wrapper
# ---------------------------------------------------------------------------


class Query:
    """Python wrapper around the native Query object that returns typed records."""

    def __init__(self, inner: _NativeQuery):
        self._inner = inner

    def select(self, *bins: str) -> None:
        self._inner.select(*bins)

    def where(self, predicate) -> None:
        self._inner.where(predicate)

    @catch_unexpected("Query.results")
    def results(self, policy=None) -> list[Record]:
        return [_wrap_record(r) for r in self._inner.results(policy)]

    @catch_unexpected("Query.foreach")
    def foreach(self, callback, policy=None) -> None:
        def _cb(raw):
            return callback(_wrap_record(raw))

        self._inner.foreach(_cb, policy)


class AsyncQuery:
    """Async Python wrapper around the native Query object that returns typed records.

    ``select()`` and ``where()`` are synchronous setup methods.
    ``results()`` and ``foreach()`` are async and run the blocking native
    query in a thread pool to avoid blocking the event loop.
    """

    def __init__(self, inner: _NativeQuery):
        self._inner = inner

    def select(self, *bins: str) -> None:
        self._inner.select(*bins)

    def where(self, predicate) -> None:
        self._inner.where(predicate)

    @catch_unexpected("AsyncQuery.results")
    async def results(self, policy=None) -> list[Record]:
        raw = await asyncio.to_thread(self._inner.results, policy)
        return [_wrap_record(r) for r in raw]

    @catch_unexpected("AsyncQuery.foreach")
    async def foreach(self, callback, policy=None) -> None:
        def _sync_foreach():
            def _cb(raw):
                return callback(_wrap_record(raw))

            self._inner.foreach(_cb, policy)

        await asyncio.to_thread(_sync_foreach)


# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------


class Client(_NativeClient):
    """Aerospike client wrapper that supports method chaining on connect().

    All read methods (``get``, ``select``, ``exists``, ``operate``, etc.)
    return NamedTuple instances (``Record``, ``ExistsResult``, etc.) with
    named field access: ``record.meta.gen``, ``record.bins["name"]``.
    """

    def connect(self, username: str | None = None, password: str | None = None) -> "Client":
        """Connect to the Aerospike cluster.

        Returns ``self`` for method chaining.

        Args:
            username: Optional username for authentication.
            password: Optional password for authentication.

        Returns:
            The connected client instance.

        Raises:
            ClusterError: Failed to connect to any cluster node.

        Example:
            ```python
            client = aerospike_py.client(config).connect()

            # With authentication
            client = aerospike_py.client(config).connect("admin", "admin")
            ```
        """
        logger.info("Connecting to Aerospike cluster")
        super().connect(username, password)
        return self

    @catch_unexpected("Client.get")
    def get(self, key, policy=None) -> Record:
        return _wrap_record(super().get(key, policy))

    @catch_unexpected("Client.select")
    def select(self, key, bins, policy=None) -> Record:
        return _wrap_record(super().select(key, bins, policy))

    @catch_unexpected("Client.exists")
    def exists(self, key, policy=None) -> ExistsResult:
        return _wrap_exists(super().exists(key, policy))

    @catch_unexpected("Client.operate")
    def operate(self, key, ops, meta=None, policy=None) -> Record:
        return _wrap_record(super().operate(key, ops, meta, policy))

    @catch_unexpected("Client.operate_ordered")
    def operate_ordered(self, key, ops, meta=None, policy=None) -> OperateOrderedResult:
        return _wrap_operate_ordered(super().operate_ordered(key, ops, meta, policy))

    @catch_unexpected("Client.info_all")
    def info_all(self, command, policy=None) -> list[InfoNodeResult]:
        return [InfoNodeResult(*t) for t in super().info_all(command, policy)]

    def batch_read(self, keys, bins=None, policy=None, _dtype=None):
        """Read multiple records in a single batch call.

        Args:
            keys: List of ``(namespace, set, primary_key)`` tuples.
            bins: Optional list of bin names to read. ``None`` reads all bins;
                an empty list performs an existence check only.
            policy: Optional batch policy dict.
            _dtype: Optional NumPy dtype. When provided, returns
                ``NumpyBatchRecords`` instead of ``BatchRecords``.

        Returns:
            ``BatchRecords`` (or ``NumpyBatchRecords`` when ``_dtype`` is set).

        Example:
            ```python
            keys = [("test", "demo", f"user_{i}") for i in range(10)]
            batch = client.batch_read(keys, bins=["name", "age"])
            for br in batch.batch_records:
                if br.record:
                    key, meta, bins = br.record  # raw tuples (not wrapped)
                    print(bins)
            ```

        Note:
            ``batch_read`` returns raw ``BatchRecords`` from the native layer.
            Individual ``br.record`` tuples are **not** wrapped as ``Record``
            NamedTuples. Use dict-style access for metadata: ``meta["gen"]``.
        """
        return super().batch_read(keys, bins, policy, _dtype)

    @catch_unexpected("Client.batch_write_numpy")
    def batch_write_numpy(self, data, namespace, set_name, _dtype, key_field="_key", policy=None):
        """Write multiple records from a numpy structured array.

        Each row of the structured array becomes a separate write operation.
        The dtype must contain a key field (default ``_key``) for the record key.
        Remaining non-underscore-prefixed fields become bins.

        Args:
            data: numpy structured array with record data.
            namespace: Target namespace.
            set_name: Target set.
            _dtype: numpy dtype describing the array layout.
            key_field: Name of the dtype field to use as the user key (default ``"_key"``).
            policy: Optional batch policy dict.

        Returns:
            A list of ``Record`` NamedTuples with write results.

        Example:
            ```python
            import numpy as np
            dtype = np.dtype([("_key", "i4"), ("score", "f8"), ("count", "i4")])
            data = np.array([(1, 0.95, 10), (2, 0.87, 20)], dtype=dtype)
            results = client.batch_write_numpy(data, "test", "demo", dtype)
            ```
        """
        return [
            _wrap_record(r) for r in super().batch_write_numpy(data, namespace, set_name, _dtype, key_field, policy)
        ]

    @catch_unexpected("Client.batch_operate")
    def batch_operate(self, keys, ops, policy=None) -> list[Record]:
        return [_wrap_record(r) for r in super().batch_operate(keys, ops, policy)]

    @catch_unexpected("Client.batch_remove")
    def batch_remove(self, keys, policy=None) -> list[Record]:
        return [_wrap_record(r) for r in super().batch_remove(keys, policy)]

    def query(self, namespace, set_name) -> Query:
        return Query(super().query(namespace, set_name))

    def __enter__(self) -> "Client":
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        logger.debug("Closing client connection")
        self.close()
        return False


class AsyncClient:
    """Aerospike async client wrapper with numpy batch_read support.

    Delegates to _NativeAsyncClient (PyO3 type that cannot be subclassed).

    All read methods (``get``, ``select``, ``exists``, ``operate``, etc.)
    return NamedTuple instances (``Record``, ``ExistsResult``, etc.) with
    named field access: ``record.meta.gen``, ``record.bins["name"]``.
    """

    def __init__(self, config: dict):
        self._inner = _NativeAsyncClient(config)

    # -- Delegate all native methods via __getattr__ --
    def __getattr__(self, name: str) -> Any:
        try:
            inner = object.__getattribute__(self, "_inner")
        except AttributeError:
            raise AttributeError(
                f"'{type(self).__name__}' object has no attribute '{name}' (client may not be fully initialized)"
            ) from None
        try:
            return getattr(inner, name)
        except AttributeError:
            raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'") from None

    async def __aenter__(self) -> "AsyncClient":
        return self

    async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> bool:
        await self.close()
        return False

    async def connect(self, username: str | None = None, password: str | None = None) -> "AsyncClient":
        """Connect to the Aerospike cluster.

        Returns ``self`` for method chaining.

        Args:
            username: Optional username for authentication.
            password: Optional password for authentication.

        Returns:
            The connected client instance.

        Raises:
            ClusterError: Failed to connect to any cluster node.

        Example:
            ```python
            client = await aerospike_py.AsyncClient(config).connect()
            await client.connect("admin", "admin")
            ```
        """
        logger.info("Async client connecting")
        await self._inner.connect(username, password)
        return self

    async def close(self) -> None:
        """Close the connection to the cluster.

        Example:
            ```python
            await client.close()
            ```
        """
        logger.debug("Async client closing")
        return await self._inner.close()

    @catch_unexpected("AsyncClient.get")
    async def get(self, key, policy=None) -> Record:
        return _wrap_record(await self._inner.get(key, policy))

    @catch_unexpected("AsyncClient.select")
    async def select(self, key, bins, policy=None) -> Record:
        return _wrap_record(await self._inner.select(key, bins, policy))

    @catch_unexpected("AsyncClient.exists")
    async def exists(self, key, policy=None) -> ExistsResult:
        return _wrap_exists(await self._inner.exists(key, policy))

    @catch_unexpected("AsyncClient.operate")
    async def operate(self, key, ops, meta=None, policy=None) -> Record:
        return _wrap_record(await self._inner.operate(key, ops, meta, policy))

    @catch_unexpected("AsyncClient.operate_ordered")
    async def operate_ordered(self, key, ops, meta=None, policy=None) -> OperateOrderedResult:
        return _wrap_operate_ordered(await self._inner.operate_ordered(key, ops, meta, policy))

    @catch_unexpected("AsyncClient.info_all")
    async def info_all(self, command, policy=None) -> list[InfoNodeResult]:
        return [InfoNodeResult(*t) for t in await self._inner.info_all(command, policy)]

    async def batch_read(
        self, keys: list, bins: list[str] | None = None, policy: dict[str, Any] | None = None, _dtype: Any = None
    ) -> Any:
        """Read multiple records in a single batch call.

        Args:
            keys: List of ``(namespace, set, primary_key)`` tuples.
            bins: Optional list of bin names to read. ``None`` reads all bins;
                an empty list performs an existence check only.
            policy: Optional batch policy dict.
            _dtype: Optional NumPy dtype. When provided, returns
                ``NumpyBatchRecords`` instead of ``BatchRecords``.

        Returns:
            ``BatchRecords`` (or ``NumpyBatchRecords`` when ``_dtype`` is set).

        Example:
            ```python
            keys = [("test", "demo", f"user_{i}") for i in range(10)]
            batch = await client.batch_read(keys, bins=["name", "age"])
            for br in batch.batch_records:
                if br.record:
                    key, meta, bins = br.record  # raw tuples (not wrapped)
                    print(bins)
            ```

        Note:
            ``batch_read`` returns raw ``BatchRecords`` from the native layer.
            Individual ``br.record`` tuples are **not** wrapped as ``Record``
            NamedTuples. Use dict-style access for metadata: ``meta["gen"]``.
        """
        return await self._inner.batch_read(keys, bins, policy, _dtype)

    @catch_unexpected("AsyncClient.batch_write_numpy")
    async def batch_write_numpy(
        self, data, namespace: str, set_name: str, _dtype, key_field: str = "_key", policy=None
    ) -> list[Record]:
        """Write multiple records from a numpy structured array (async).

        Each row of the structured array becomes a separate write operation.
        The dtype must contain a key field (default ``_key``) for the record key.
        Remaining non-underscore-prefixed fields become bins.

        Args:
            data: numpy structured array with record data.
            namespace: Target namespace.
            set_name: Target set.
            _dtype: numpy dtype describing the array layout.
            key_field: Name of the dtype field to use as the user key (default ``"_key"``).
            policy: Optional batch policy dict.

        Returns:
            A list of ``Record`` NamedTuples with write results.

        Example:
            ```python
            import numpy as np
            dtype = np.dtype([("_key", "i4"), ("score", "f8"), ("count", "i4")])
            data = np.array([(1, 0.95, 10), (2, 0.87, 20)], dtype=dtype)
            results = await client.batch_write_numpy(data, "test", "demo", dtype)
            ```
        """
        return [
            _wrap_record(r)
            for r in await self._inner.batch_write_numpy(data, namespace, set_name, _dtype, key_field, policy)
        ]

    @catch_unexpected("AsyncClient.batch_operate")
    async def batch_operate(self, keys, ops, policy=None) -> list[Record]:
        return [_wrap_record(r) for r in await self._inner.batch_operate(keys, ops, policy)]

    @catch_unexpected("AsyncClient.batch_remove")
    async def batch_remove(self, keys, policy=None) -> list[Record]:
        return [_wrap_record(r) for r in await self._inner.batch_remove(keys, policy)]

    def is_connected(self) -> bool:
        return self._inner.is_connected()

    async def get_node_names(self) -> list[str]:
        return await self._inner.get_node_names()

    async def info_random_node(self, command, policy=None) -> str:
        return await self._inner.info_random_node(command, policy)

    async def put(self, key, bins, meta=None, policy=None) -> None:
        return await self._inner.put(key, bins, meta=meta, policy=policy)

    async def remove(self, key, meta=None, policy=None) -> None:
        return await self._inner.remove(key, meta=meta, policy=policy)

    async def touch(self, key, val=0, meta=None, policy=None) -> None:
        return await self._inner.touch(key, val=val, meta=meta, policy=policy)

    async def append(self, key, bin, val, meta=None, policy=None) -> None:
        return await self._inner.append(key, bin, val, meta=meta, policy=policy)

    async def prepend(self, key, bin, val, meta=None, policy=None) -> None:
        return await self._inner.prepend(key, bin, val, meta=meta, policy=policy)

    async def increment(self, key, bin, offset, meta=None, policy=None) -> None:
        return await self._inner.increment(key, bin, offset, meta=meta, policy=policy)

    async def remove_bin(self, key, bin_names, meta=None, policy=None) -> None:
        return await self._inner.remove_bin(key, bin_names, meta=meta, policy=policy)

    # -- Index --

    async def index_integer_create(self, namespace, set_name, bin_name, index_name, policy=None) -> None:
        return await self._inner.index_integer_create(namespace, set_name, bin_name, index_name, policy)

    async def index_string_create(self, namespace, set_name, bin_name, index_name, policy=None) -> None:
        return await self._inner.index_string_create(namespace, set_name, bin_name, index_name, policy)

    async def index_geo2dsphere_create(self, namespace, set_name, bin_name, index_name, policy=None) -> None:
        return await self._inner.index_geo2dsphere_create(namespace, set_name, bin_name, index_name, policy)

    async def index_remove(self, namespace, index_name, policy=None) -> None:
        return await self._inner.index_remove(namespace, index_name, policy)

    # -- Truncate --

    async def truncate(self, namespace, set_name, nanos=0, policy=None) -> None:
        return await self._inner.truncate(namespace, set_name, nanos, policy)

    # -- UDF --

    async def udf_put(self, filename, udf_type=0, policy=None) -> None:
        return await self._inner.udf_put(filename, udf_type, policy)

    async def udf_remove(self, module, policy=None) -> None:
        return await self._inner.udf_remove(module, policy)

    async def apply(self, key, module, function, args=None, policy=None):
        return await self._inner.apply(key, module, function, args, policy)

    # -- Admin: User --

    async def admin_create_user(self, username, password, roles, policy=None) -> None:
        return await self._inner.admin_create_user(username, password, roles, policy)

    async def admin_drop_user(self, username, policy=None) -> None:
        return await self._inner.admin_drop_user(username, policy)

    async def admin_change_password(self, username, password, policy=None) -> None:
        return await self._inner.admin_change_password(username, password, policy)

    async def admin_grant_roles(self, username, roles, policy=None) -> None:
        return await self._inner.admin_grant_roles(username, roles, policy)

    async def admin_revoke_roles(self, username, roles, policy=None) -> None:
        return await self._inner.admin_revoke_roles(username, roles, policy)

    async def admin_query_user_info(self, username, policy=None):
        return await self._inner.admin_query_user_info(username, policy)

    async def admin_query_users_info(self, policy=None):
        return await self._inner.admin_query_users_info(policy)

    # -- Admin: Role --

    async def admin_create_role(
        self, role, privileges, policy=None, whitelist=None, read_quota=0, write_quota=0
    ) -> None:
        return await self._inner.admin_create_role(role, privileges, policy, whitelist, read_quota, write_quota)

    async def admin_drop_role(self, role, policy=None) -> None:
        return await self._inner.admin_drop_role(role, policy)

    async def admin_grant_privileges(self, role, privileges, policy=None) -> None:
        return await self._inner.admin_grant_privileges(role, privileges, policy)

    async def admin_revoke_privileges(self, role, privileges, policy=None) -> None:
        return await self._inner.admin_revoke_privileges(role, privileges, policy)

    async def admin_query_role(self, role, policy=None):
        return await self._inner.admin_query_role(role, policy)

    async def admin_query_roles(self, policy=None):
        return await self._inner.admin_query_roles(policy)

    async def admin_set_whitelist(self, role, whitelist, policy=None) -> None:
        return await self._inner.admin_set_whitelist(role, whitelist, policy)

    async def admin_set_quotas(self, role, read_quota=0, write_quota=0, policy=None) -> None:
        return await self._inner.admin_set_quotas(role, read_quota, write_quota, policy)

    # -- Query --

    def query(self, namespace: str, set_name: str) -> AsyncQuery:
        """Create a query object for the given namespace and set.

        Returns an ``AsyncQuery`` whose ``results()`` and ``foreach()``
        methods are coroutines.

        Args:
            namespace: The namespace to query.
            set_name: The set name to query.

        Returns:
            An ``AsyncQuery`` instance.

        Example:
            ```python
            query = client.query("test", "demo")
            query.where(predicates.between("age", 20, 30))
            records = await query.results()
            ```
        """
        return AsyncQuery(self._inner.query(namespace, set_name))


_LEVEL_MAP: dict[int, int] = {
    -1: logging.CRITICAL + 1,  # OFF
    0: logging.ERROR,
    1: logging.WARNING,
    2: logging.INFO,
    3: logging.DEBUG,
    4: 5,  # TRACE
}
"""Map aerospike LOG_LEVEL_* constants to Python logging levels."""


def set_log_level(level: int) -> None:
    """Set the aerospike_py log level.

    Accepts ``LOG_LEVEL_*`` constants. Controls both Rust-internal
    and Python-side logging.

    Args:
        level: One of ``LOG_LEVEL_OFF`` (-1), ``LOG_LEVEL_ERROR`` (0),
            ``LOG_LEVEL_WARN`` (1), ``LOG_LEVEL_INFO`` (2),
            ``LOG_LEVEL_DEBUG`` (3), ``LOG_LEVEL_TRACE`` (4).

    Example:
        ```python
        import aerospike_py

        aerospike_py.set_log_level(aerospike_py.LOG_LEVEL_DEBUG)
        ```
    """
    py_level = _LEVEL_MAP.get(level, level)
    logging.getLogger("aerospike_py").setLevel(py_level)
    logging.getLogger("_aerospike").setLevel(py_level)
    logging.getLogger("aerospike_core").setLevel(py_level)
    logging.getLogger("aerospike").setLevel(py_level)


def client(config: dict) -> Client:
    """Create a new Aerospike client instance.

    Args:
        config: Configuration dictionary. Must contain a ``"hosts"`` key
            with a list of ``(host, port)`` tuples.

    Returns:
        A new ``Client`` instance (not yet connected).

    Example:
        ```python
        import aerospike_py

        client = aerospike_py.client({
            "hosts": [("127.0.0.1", 3000)],
        }).connect()
        ```
    """
    return Client(config)


def async_client(config: dict) -> AsyncClient:
    """Create a new async Aerospike client instance.

    Args:
        config: Configuration dictionary. Must contain a ``"hosts"`` key
            with a list of ``(host, port)`` tuples.

    Returns:
        A new ``AsyncClient`` instance (not yet connected).

    Example:
        ```python
        import aerospike_py

        client = aerospike_py.async_client({
            "hosts": [("127.0.0.1", 3000)],
        })
        await client.connect()
        ```
    """
    return AsyncClient(config)


def get_metrics() -> str:
    """Return collected metrics in Prometheus text format."""
    return _get_metrics_text()


def set_metrics_enabled(enabled: bool) -> None:
    """Enable or disable Prometheus metrics collection.

    When disabled, operation timers are skipped entirely (~1ns atomic check).
    Useful for benchmarking without metrics overhead.

    Args:
        enabled: ``True`` to enable (default), ``False`` to disable.

    Example:
        ```python
        aerospike_py.set_metrics_enabled(False)   # disable metrics
        # ... run benchmark ...
        aerospike_py.set_metrics_enabled(True)     # re-enable
        ```
    """
    _set_metrics_enabled(enabled)


def is_metrics_enabled() -> bool:
    """Check if Prometheus metrics collection is currently enabled.

    Returns:
        ``True`` if metrics are enabled (default), ``False`` otherwise.
    """
    return _is_metrics_enabled()


_metrics_server = None
_metrics_server_thread = None
_metrics_lock = threading.Lock()


class _MetricsHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/metrics":
            body = _get_metrics_text().encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, format, *args):
        pass


def start_metrics_server(port: int = 9464) -> None:
    """Start a background HTTP server serving /metrics for Prometheus scraping."""
    global _metrics_server, _metrics_server_thread

    with _metrics_lock:
        new_server = HTTPServer(("", port), _MetricsHandler)

        if _metrics_server is not None:
            _metrics_server.shutdown()

        _metrics_server = new_server
        _metrics_server_thread = threading.Thread(target=_metrics_server.serve_forever, daemon=True)
        _metrics_server_thread.start()


def stop_metrics_server() -> None:
    """Stop the background metrics HTTP server."""
    global _metrics_server, _metrics_server_thread

    with _metrics_lock:
        if _metrics_server is not None:
            try:
                _metrics_server.shutdown()
                if _metrics_server_thread is not None:
                    _metrics_server_thread.join(timeout=5)
                    if _metrics_server_thread.is_alive():
                        logger.warning(
                            "Metrics server thread did not stop within 5 seconds; "
                            "thread is daemonic and will be terminated at interpreter exit"
                        )
            finally:
                _metrics_server = None
                _metrics_server_thread = None


def init_tracing() -> None:
    """Initialize OpenTelemetry tracing.

    Reads standard OTEL_* environment variables for configuration.
    Key variables:
        OTEL_EXPORTER_OTLP_ENDPOINT  - gRPC endpoint (default: http://localhost:4317)
        OTEL_SERVICE_NAME            - service name (default: aerospike-py)
        OTEL_SDK_DISABLED=true       - disable tracing entirely
        OTEL_TRACES_EXPORTER=none    - disable trace export
    """
    _init_tracing()


def shutdown_tracing() -> None:
    """Shut down the tracer provider, flushing pending spans.

    Call before process exit to ensure all spans are exported.
    """
    _shutdown_tracing()


__all__ = [
    # Core classes and factory
    "Client",
    "AsyncClient",
    "Query",
    "AsyncQuery",
    "BatchRecord",
    "BatchRecords",
    "NumpyBatchRecords",
    "client",
    "async_client",
    "set_log_level",
    "get_metrics",
    "set_metrics_enabled",
    "is_metrics_enabled",
    "start_metrics_server",
    "stop_metrics_server",
    "init_tracing",
    "shutdown_tracing",
    "__version__",
    # Type classes
    "AerospikeKey",
    "RecordMetadata",
    "Record",
    "ExistsResult",
    "InfoNodeResult",
    "BinTuple",
    "OperateOrderedResult",
    "Bins",
    "ReadPolicy",
    "WritePolicy",
    "BatchPolicy",
    "AdminPolicy",
    "QueryPolicy",
    "WriteMeta",
    "ClientConfig",
    "Privilege",
    "UserInfo",
    "RoleInfo",
    "ListPolicy",
    "MapPolicy",
    "Operation",
    # Submodules
    "exception",
    "predicates",
    "list_operations",
    "map_operations",
    "exp",
    # Exception classes
    "AerospikeError",
    "ClientError",
    "ClusterError",
    "InvalidArgError",
    "RecordError",
    "ServerError",
    "AerospikeTimeoutError",
    "TimeoutError",  # deprecated alias
    "RecordNotFound",
    "RecordExistsError",
    "RecordGenerationError",
    "RecordTooBig",
    "BinNameError",
    "BinExistsError",
    "BinNotFound",
    "BinTypeError",
    "FilteredOut",
    "AerospikeIndexError",
    "IndexError",  # deprecated alias
    "IndexNotFound",
    "IndexFoundError",
    "QueryError",
    "QueryAbortedError",
    "AdminError",
    "UDFError",
    # Policy Key
    "POLICY_KEY_DIGEST",
    "POLICY_KEY_SEND",
    # Policy Exists
    "POLICY_EXISTS_IGNORE",
    "POLICY_EXISTS_UPDATE",
    "POLICY_EXISTS_UPDATE_ONLY",
    "POLICY_EXISTS_REPLACE",
    "POLICY_EXISTS_REPLACE_ONLY",
    "POLICY_EXISTS_CREATE_ONLY",
    # Policy Gen
    "POLICY_GEN_IGNORE",
    "POLICY_GEN_EQ",
    "POLICY_GEN_GT",
    # Policy Replica
    "POLICY_REPLICA_MASTER",
    "POLICY_REPLICA_SEQUENCE",
    "POLICY_REPLICA_PREFER_RACK",
    # Policy Commit Level
    "POLICY_COMMIT_LEVEL_ALL",
    "POLICY_COMMIT_LEVEL_MASTER",
    # Policy Read Mode AP
    "POLICY_READ_MODE_AP_ONE",
    "POLICY_READ_MODE_AP_ALL",
    # TTL Constants
    "TTL_NAMESPACE_DEFAULT",
    "TTL_NEVER_EXPIRE",
    "TTL_DONT_UPDATE",
    "TTL_CLIENT_DEFAULT",
    # Auth Mode
    "AUTH_INTERNAL",
    "AUTH_EXTERNAL",
    "AUTH_PKI",
    # Operator Constants
    "OPERATOR_READ",
    "OPERATOR_WRITE",
    "OPERATOR_INCR",
    "OPERATOR_APPEND",
    "OPERATOR_PREPEND",
    "OPERATOR_TOUCH",
    "OPERATOR_DELETE",
    # Index Type
    "INDEX_NUMERIC",
    "INDEX_STRING",
    "INDEX_BLOB",
    "INDEX_GEO2DSPHERE",
    # Index Collection Type
    "INDEX_TYPE_DEFAULT",
    "INDEX_TYPE_LIST",
    "INDEX_TYPE_MAPKEYS",
    "INDEX_TYPE_MAPVALUES",
    # Log Level
    "LOG_LEVEL_OFF",
    "LOG_LEVEL_ERROR",
    "LOG_LEVEL_WARN",
    "LOG_LEVEL_INFO",
    "LOG_LEVEL_DEBUG",
    "LOG_LEVEL_TRACE",
    # Serializer
    "SERIALIZER_NONE",
    "SERIALIZER_PYTHON",
    "SERIALIZER_USER",
    # List Return Type
    "LIST_RETURN_NONE",
    "LIST_RETURN_INDEX",
    "LIST_RETURN_REVERSE_INDEX",
    "LIST_RETURN_RANK",
    "LIST_RETURN_REVERSE_RANK",
    "LIST_RETURN_COUNT",
    "LIST_RETURN_VALUE",
    "LIST_RETURN_EXISTS",
    # List Order
    "LIST_UNORDERED",
    "LIST_ORDERED",
    # List Sort Flags
    "LIST_SORT_DEFAULT",
    "LIST_SORT_DROP_DUPLICATES",
    # List Write Flags
    "LIST_WRITE_DEFAULT",
    "LIST_WRITE_ADD_UNIQUE",
    "LIST_WRITE_INSERT_BOUNDED",
    "LIST_WRITE_NO_FAIL",
    "LIST_WRITE_PARTIAL",
    # Map Return Type
    "MAP_RETURN_NONE",
    "MAP_RETURN_INDEX",
    "MAP_RETURN_REVERSE_INDEX",
    "MAP_RETURN_RANK",
    "MAP_RETURN_REVERSE_RANK",
    "MAP_RETURN_COUNT",
    "MAP_RETURN_KEY",
    "MAP_RETURN_VALUE",
    "MAP_RETURN_KEY_VALUE",
    "MAP_RETURN_EXISTS",
    # Map Order
    "MAP_UNORDERED",
    "MAP_KEY_ORDERED",
    "MAP_KEY_VALUE_ORDERED",
    # Map Write Flags
    "MAP_WRITE_FLAGS_DEFAULT",
    "MAP_WRITE_FLAGS_CREATE_ONLY",
    "MAP_WRITE_FLAGS_UPDATE_ONLY",
    "MAP_WRITE_FLAGS_NO_FAIL",
    "MAP_WRITE_FLAGS_PARTIAL",
    "MAP_UPDATE",
    "MAP_UPDATE_ONLY",
    "MAP_CREATE_ONLY",
    # Bit Write Flags
    "BIT_WRITE_DEFAULT",
    "BIT_WRITE_CREATE_ONLY",
    "BIT_WRITE_UPDATE_ONLY",
    "BIT_WRITE_NO_FAIL",
    "BIT_WRITE_PARTIAL",
    # HLL Write Flags
    "HLL_WRITE_DEFAULT",
    "HLL_WRITE_CREATE_ONLY",
    "HLL_WRITE_UPDATE_ONLY",
    "HLL_WRITE_NO_FAIL",
    "HLL_WRITE_ALLOW_FOLD",
    # Privilege codes
    "PRIV_READ",
    "PRIV_WRITE",
    "PRIV_READ_WRITE",
    "PRIV_READ_WRITE_UDF",
    "PRIV_SYS_ADMIN",
    "PRIV_USER_ADMIN",
    "PRIV_DATA_ADMIN",
    "PRIV_UDF_ADMIN",
    "PRIV_SINDEX_ADMIN",
    "PRIV_TRUNCATE",
    # Status codes
    "AEROSPIKE_OK",
    "AEROSPIKE_ERR_SERVER",
    "AEROSPIKE_ERR_RECORD_NOT_FOUND",
    "AEROSPIKE_ERR_RECORD_GENERATION",
    "AEROSPIKE_ERR_PARAM",
    "AEROSPIKE_ERR_RECORD_EXISTS",
    "AEROSPIKE_ERR_BIN_EXISTS",
    "AEROSPIKE_ERR_CLUSTER_KEY_MISMATCH",
    "AEROSPIKE_ERR_SERVER_MEM",
    "AEROSPIKE_ERR_TIMEOUT",
    "AEROSPIKE_ERR_ALWAYS_FORBIDDEN",
    "AEROSPIKE_ERR_PARTITION_UNAVAILABLE",
    "AEROSPIKE_ERR_BIN_TYPE",
    "AEROSPIKE_ERR_RECORD_TOO_BIG",
    "AEROSPIKE_ERR_KEY_BUSY",
    "AEROSPIKE_ERR_SCAN_ABORT",
    "AEROSPIKE_ERR_UNSUPPORTED_FEATURE",
    "AEROSPIKE_ERR_BIN_NOT_FOUND",
    "AEROSPIKE_ERR_DEVICE_OVERLOAD",
    "AEROSPIKE_ERR_KEY_MISMATCH",
    "AEROSPIKE_ERR_INVALID_NAMESPACE",
    "AEROSPIKE_ERR_BIN_NAME",
    "AEROSPIKE_ERR_FAIL_FORBIDDEN",
    "AEROSPIKE_ERR_ELEMENT_NOT_FOUND",
    "AEROSPIKE_ERR_ELEMENT_EXISTS",
    "AEROSPIKE_ERR_ENTERPRISE_ONLY",
    "AEROSPIKE_ERR_OP_NOT_APPLICABLE",
    "AEROSPIKE_ERR_FILTERED_OUT",
    "AEROSPIKE_ERR_LOST_CONFLICT",
    "AEROSPIKE_QUERY_END",
    "AEROSPIKE_SECURITY_NOT_SUPPORTED",
    "AEROSPIKE_SECURITY_NOT_ENABLED",
    "AEROSPIKE_ERR_INVALID_USER",
    "AEROSPIKE_ERR_NOT_AUTHENTICATED",
    "AEROSPIKE_ERR_ROLE_VIOLATION",
    "AEROSPIKE_ERR_UDF",
    "AEROSPIKE_ERR_BATCH_DISABLED",
    "AEROSPIKE_ERR_INDEX_FOUND",
    "AEROSPIKE_ERR_INDEX_NOT_FOUND",
    "AEROSPIKE_ERR_QUERY_ABORTED",
    "AEROSPIKE_ERR_CLIENT",
    "AEROSPIKE_ERR_CONNECTION",
    "AEROSPIKE_ERR_CLUSTER",
    "AEROSPIKE_ERR_INVALID_HOST",
    "AEROSPIKE_ERR_NO_MORE_CONNECTIONS",
]
