"""Async wrapper around MemoryFileSystem.

All I/O is delegated to :func:`asyncio.to_thread`, so the underlying
synchronous locks are never held on the event-loop thread.
"""

from __future__ import annotations

import asyncio
import io

from ._fs import MemoryFileSystem
from ._typing import MFSStatResult, MFSStats


class AsyncMemoryFileHandle:
    """Async wrapper for a single open-file handle."""

    def __init__(self, _sync_handle) -> None:  # type: ignore[no-untyped-def]
        self._h = _sync_handle

    async def read(self, size: int = -1) -> bytes:
        """Read bytes.  See :meth:`MemoryFileHandle.read`."""
        return await asyncio.to_thread(self._h.read, size)

    async def write(self, data: bytes) -> int:
        """Write bytes.  See :meth:`MemoryFileHandle.write`."""
        return await asyncio.to_thread(self._h.write, data)

    async def seek(self, offset: int, whence: int = 0) -> int:
        """Set stream position.  See :meth:`MemoryFileHandle.seek`."""
        return await asyncio.to_thread(self._h.seek, offset, whence)

    async def tell(self) -> int:
        """Return current stream position.  See :meth:`MemoryFileHandle.tell`."""
        return await asyncio.to_thread(self._h.tell)

    async def truncate(self, size: int | None = None) -> int:
        """Resize the file.  See :meth:`MemoryFileHandle.truncate`."""
        return await asyncio.to_thread(self._h.truncate, size)

    async def flush(self) -> None:
        """Flush the write buffer (no-op for MFS).  See :meth:`MemoryFileHandle.flush`."""
        await asyncio.to_thread(self._h.flush)

    async def readable(self) -> bool:
        """Return ``True`` if the handle supports reading."""
        return await asyncio.to_thread(self._h.readable)

    async def writable(self) -> bool:
        """Return ``True`` if the handle supports writing."""
        return await asyncio.to_thread(self._h.writable)

    async def seekable(self) -> bool:
        """Return ``True`` (MFS handles are always seekable)."""
        return await asyncio.to_thread(self._h.seekable)

    async def close(self) -> None:
        """Release the file lock.  See :meth:`MemoryFileHandle.close`."""
        await asyncio.to_thread(self._h.close)

    async def __aenter__(self) -> AsyncMemoryFileHandle:
        return self

    async def __aexit__(self, *args) -> None:  # type: ignore[no-untyped-def]
        await self.close()


class AsyncMemoryFileSystem:
    """Thin async facade over :class:`MemoryFileSystem`.

    Every method delegates to the synchronous implementation via
    ``asyncio.to_thread``, so the event-loop is never blocked.
    """

    def __init__(
        self,
        max_quota: int = 256 * 1024 * 1024,
        chunk_overhead_override: int | None = None,
        promotion_hard_limit: int | None = None,
        max_nodes: int | None = None,
        default_storage: str = "auto",
        default_lock_timeout: float | None = 30.0,
        memory_guard: str = "none",
        memory_guard_action: str = "warn",
        memory_guard_interval: float = 1.0,
    ) -> None:
        """
        Parameters
        ----------
        max_quota : int
            See :class:`MemoryFileSystem`.
        chunk_overhead_override : int | None
            See :class:`MemoryFileSystem`.
        promotion_hard_limit : int | None
            See :class:`MemoryFileSystem`.
        max_nodes : int | None
            See :class:`MemoryFileSystem`.
        default_storage : str
            See :class:`MemoryFileSystem`.
        default_lock_timeout : float | None
            See :class:`MemoryFileSystem`.  Default for async is ``30.0``.
        memory_guard : str
            See :class:`MemoryFileSystem`.
        memory_guard_action : str
            See :class:`MemoryFileSystem`.
        memory_guard_interval : float
            See :class:`MemoryFileSystem`.
        """
        self._sync = MemoryFileSystem(
            max_quota=max_quota,
            chunk_overhead_override=chunk_overhead_override,
            promotion_hard_limit=promotion_hard_limit,
            max_nodes=max_nodes,
            default_storage=default_storage,
            default_lock_timeout=default_lock_timeout,
            memory_guard=memory_guard,
            memory_guard_action=memory_guard_action,
            memory_guard_interval=memory_guard_interval,
        )

    async def open(
        self,
        path: str,
        mode: str = "rb",
        preallocate: int = 0,
        lock_timeout: float | None = None,
    ) -> AsyncMemoryFileHandle:
        """Open a file and return an :class:`AsyncMemoryFileHandle`.

        All parameters and exceptions are identical to
        :meth:`MemoryFileSystem.open`; the operation is offloaded to a thread
        via :func:`asyncio.to_thread`.

        Returns
        -------
        AsyncMemoryFileHandle
            Use with ``async with`` to ensure the handle is closed::

                async with await mfs.open("/f.bin", "wb") as f:
                    await f.write(b"data")
        """
        h = await asyncio.to_thread(self._sync.open, path, mode, preallocate, lock_timeout)
        return AsyncMemoryFileHandle(h)

    async def mkdir(self, path: str, exist_ok: bool = False) -> None:
        """Create a directory.  See :meth:`MemoryFileSystem.mkdir`."""
        await asyncio.to_thread(self._sync.mkdir, path, exist_ok)

    async def rename(self, src: str, dst: str) -> None:
        """Rename or move a file or directory.  See :meth:`MemoryFileSystem.rename`."""
        await asyncio.to_thread(self._sync.rename, src, dst)

    async def move(self, src: str, dst: str) -> None:
        """Move a file or directory, creating parents as needed.  See :meth:`MemoryFileSystem.move`."""
        await asyncio.to_thread(self._sync.move, src, dst)

    async def remove(self, path: str) -> None:
        """Delete a single file.  See :meth:`MemoryFileSystem.remove`."""
        await asyncio.to_thread(self._sync.remove, path)

    async def rmtree(self, path: str) -> None:
        """Recursively delete a directory.  See :meth:`MemoryFileSystem.rmtree`."""
        await asyncio.to_thread(self._sync.rmtree, path)

    async def listdir(self, path: str) -> list[str]:
        """Return entry names in a directory.  See :meth:`MemoryFileSystem.listdir`."""
        return await asyncio.to_thread(self._sync.listdir, path)

    async def exists(self, path: str) -> bool:
        """Return ``True`` if *path* exists.  See :meth:`MemoryFileSystem.exists`."""
        return await asyncio.to_thread(self._sync.exists, path)

    async def is_dir(self, path: str) -> bool:
        """Return ``True`` if *path* is a directory.  See :meth:`MemoryFileSystem.is_dir`."""
        return await asyncio.to_thread(self._sync.is_dir, path)

    async def is_file(self, path: str) -> bool:
        """Return ``True`` if *path* is a regular file.  See :meth:`MemoryFileSystem.is_file`."""
        return await asyncio.to_thread(self._sync.is_file, path)

    async def stat(self, path: str) -> MFSStatResult:
        """Return metadata for *path*.  See :meth:`MemoryFileSystem.stat`."""
        return await asyncio.to_thread(self._sync.stat, path)

    async def stats(self) -> MFSStats:
        """Return aggregate filesystem statistics.  See :meth:`MemoryFileSystem.stats`."""
        return await asyncio.to_thread(self._sync.stats)

    async def get_size(self, path: str) -> int:
        """Return the size of a file in bytes.  See :meth:`MemoryFileSystem.get_size`."""
        return await asyncio.to_thread(self._sync.get_size, path)

    async def export_as_bytesio(self, path: str, max_size: int | None = None) -> io.BytesIO:
        """Export file contents as ``BytesIO``.  See :meth:`MemoryFileSystem.export_as_bytesio`."""
        return await asyncio.to_thread(self._sync.export_as_bytesio, path, max_size)

    async def export_tree(self, prefix: str = "/", only_dirty: bool = False) -> dict[str, bytes]:
        """Export files as a dict.  See :meth:`MemoryFileSystem.export_tree`."""
        return await asyncio.to_thread(self._sync.export_tree, prefix, only_dirty)

    async def import_tree(self, tree: dict[str, bytes]) -> None:
        """Import a dict of files atomically.  See :meth:`MemoryFileSystem.import_tree`."""
        await asyncio.to_thread(self._sync.import_tree, tree)

    async def copy(self, src: str, dst: str) -> None:
        """Copy a single file.  See :meth:`MemoryFileSystem.copy`."""
        await asyncio.to_thread(self._sync.copy, src, dst)

    async def copy_tree(self, src: str, dst: str) -> None:
        """Deep-copy a directory subtree.  See :meth:`MemoryFileSystem.copy_tree`."""
        await asyncio.to_thread(self._sync.copy_tree, src, dst)

    async def walk(self, path: str = "/") -> list[tuple[str, list[str], list[str]]]:
        """Walk the directory tree and return results as a list.

        Unlike the synchronous :meth:`MemoryFileSystem.walk` generator, this
        method collects all results into a list before returning (the
        generator cannot be safely iterated across thread boundaries).

        Returns
        -------
        list[tuple[str, list[str], list[str]]]
            Same structure as :func:`os.walk` top-down results.
        """
        return await asyncio.to_thread(lambda: list(self._sync.walk(path)))

    async def glob(self, pattern: str) -> list[str]:
        """Return paths matching *pattern*.  See :meth:`MemoryFileSystem.glob`."""
        return await asyncio.to_thread(self._sync.glob, pattern)
