from __future__ import annotations

import io
import time
import warnings
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from ._fs import FileNode, MemoryFileSystem


class MemoryFileHandle(io.RawIOBase):
    """Binary file handle returned by :meth:`MemoryFileSystem.open`.

    Wraps a :class:`FileNode` and provides standard stream semantics over
    MFS in-memory storage.  Inherits :class:`io.RawIOBase`.

    The underlying file lock (read or write) is held for the **entire**
    lifetime of the handle — from :meth:`MemoryFileSystem.open` until
    :meth:`close`.  Always use the ``with`` statement to ensure timely
    release::

        with mfs.open("/data.bin", "rb") as f:
            data = f.read()

    Do **not** instantiate this class directly; always obtain it through
    :meth:`MemoryFileSystem.open`.
    """

    def __init__(
        self,
        mfs: MemoryFileSystem,
        fnode: FileNode,
        path: str,
        mode: str,
        is_append: bool = False,
    ) -> None:
        super().__init__()
        self._mfs = mfs
        self._fnode = fnode
        self._path = path
        self._mode = mode
        self._cursor: int = fnode.storage.get_size() if is_append else 0
        self._is_closed: bool = False
        self._is_append: bool = is_append

    def _assert_readable(self) -> None:
        if self._mode in ("wb", "ab", "xb"):
            raise io.UnsupportedOperation(f"not readable in mode '{self._mode}'")

    def _assert_writable(self) -> None:
        if self._mode == "rb":
            raise io.UnsupportedOperation(f"not writable in mode '{self._mode}'")

    def _assert_open(self) -> None:
        if self.closed or self._is_closed:
            raise ValueError("I/O operation on closed file.")

    def read(self, size: int = -1) -> bytes:
        """Read and return up to *size* bytes.

        Parameters
        ----------
        size : int
            Maximum bytes to read.  ``-1`` (default) reads to EOF.

        Returns
        -------
        bytes
            The data read.  Returns ``b""`` when the cursor is already at or
            past EOF.

        Raises
        ------
        ValueError
            The handle is closed.
        io.UnsupportedOperation
            The handle was opened in a write-only mode
            (``"wb"``, ``"ab"``, or ``"xb"``).
        """
        self._assert_open()
        self._assert_readable()
        storage = self._fnode.storage
        current_size = storage.get_size()
        if self._cursor >= current_size:
            return b""
        if size < 0:
            data = storage.read_at(self._cursor, current_size - self._cursor)
            self._cursor = current_size
        else:
            actual = min(size, current_size - self._cursor)
            data = storage.read_at(self._cursor, actual)
            self._cursor += actual
        return data

    def write(self, data: Any) -> int:
        """Write *data* to the file and return the number of bytes written.

        For ``"ab"`` mode, the cursor is automatically moved to EOF before
        each write (POSIX append semantics).

        Quota is charged only for the net increase in file size.
        Overwriting existing bytes (cursor + len(data) ≤ current size) does
        not consume additional quota.

        Parameters
        ----------
        data : bytes | bytearray | memoryview
            Data to write.

        Returns
        -------
        int
            Number of bytes written (always ``len(data)`` unless the storage
            raises).

        Raises
        ------
        TypeError
            *data* is not a bytes-like object.
        ValueError
            The handle is closed.
        io.UnsupportedOperation
            The handle was opened in read-only mode (``"rb"``).
        MFSQuotaExceededError
            Writing *data* would exceed the filesystem quota.
        """
        self._assert_open()
        self._assert_writable()
        if not isinstance(data, (bytes, bytearray, memoryview)):
            raise TypeError("a bytes-like object is required")
        payload = bytes(data)
        if self._is_append:
            self._cursor = self._fnode.storage.get_size()
        n, promoted, old_quota = self._fnode.storage.write_at(
            self._cursor, payload, self._mfs._quota, self._mfs._memory_guard
        )
        if promoted is not None:
            self._fnode.storage = promoted
            self._mfs._quota.release(old_quota)
        self._cursor += n
        if n > 0:
            self._fnode.generation += 1
            self._fnode.modified_at = time.time()
        return n

    def readinto(self, buffer: Any) -> int:
        self._assert_open()
        self._assert_readable()
        view = memoryview(buffer).cast("B")
        data = self.read(len(view))
        n = len(data)
        view[:n] = data
        return n

    def seek(self, offset: int, whence: int = 0) -> int:
        """Set the stream position and return the new absolute position.

        Parameters
        ----------
        offset : int
            Byte offset relative to the position indicated by *whence*.
        whence : int
            Positioning anchor:

            * ``0`` / ``io.SEEK_SET`` — relative to file start;
              *offset* must be ≥ 0.
            * ``1`` / ``io.SEEK_CUR`` — relative to current position.
            * ``2`` / ``io.SEEK_END`` — relative to EOF;
              *offset* must be ≤ 0 (seeking *past* EOF is not supported).

        Returns
        -------
        int
            New absolute stream position.

        Raises
        ------
        ValueError
            *whence* is not 0, 1, or 2; or the resulting position is
            negative; or a positive *offset* is used with ``SEEK_END``.
        ValueError
            The handle is closed.
        """
        self._assert_open()
        if whence == 0:
            if offset < 0:
                raise ValueError("seek offset must be >= 0 for SEEK_SET")
            new_pos = offset
        elif whence == 1:
            new_pos = self._cursor + offset
        elif whence == 2:
            if offset > 0:
                raise ValueError(
                    "Seeking past end-of-file (SEEK_END with positive offset) "
                    "is not supported in MFS."
                )
            new_pos = self._fnode.storage.get_size() + offset
        else:
            raise ValueError(f"Invalid whence value: {whence}. Must be 0, 1, or 2.")
        if new_pos < 0:
            raise ValueError(f"Resulting cursor position {new_pos} is negative.")
        self._cursor = new_pos
        return self._cursor

    def tell(self) -> int:
        """Return the current stream position.

        Returns
        -------
        int
            Current byte offset from the beginning of the file.

        Raises
        ------
        ValueError
            The handle is closed.
        """
        self._assert_open()
        return self._cursor

    def truncate(self, size: int | None = None) -> int:
        """Resize the file to exactly *size* bytes and return the new size.

        If *size* is larger than the current file size, the file is extended
        with null bytes (POSIX semantics).  If *size* is smaller, excess data
        is discarded and the freed quota is released immediately.

        Parameters
        ----------
        size : int | None
            Target size in bytes.  ``None`` uses the current cursor position.
            Must be ≥ 0.

        Returns
        -------
        int
            The new file size.

        Raises
        ------
        ValueError
            The handle is closed, or *size* is negative.
        io.UnsupportedOperation
            The handle was opened in read-only mode (``"rb"``).
        MFSQuotaExceededError
            Extending the file would exceed the filesystem quota.
        """
        self._assert_open()
        self._assert_writable()
        target = self._cursor if size is None else size
        if target < 0:
            raise ValueError("truncate size must be >= 0")
        before = self._fnode.storage.get_size()
        self._fnode.storage.truncate(target, self._mfs._quota, self._mfs._memory_guard)
        if self._cursor > target:
            self._cursor = target
        if before != target:
            self._fnode.generation += 1
            self._fnode.modified_at = time.time()
        return target

    def flush(self) -> None:
        self._assert_open()
        return None

    def readable(self) -> bool:
        self._assert_open()
        return self._mode not in ("wb", "ab", "xb")

    def writable(self) -> bool:
        self._assert_open()
        return self._mode != "rb"

    def seekable(self) -> bool:
        self._assert_open()
        return True

    def close(self) -> None:
        """Flush and release the file lock.

        Subsequent operations on a closed handle raise :exc:`ValueError`.
        Calling ``close()`` on an already-closed handle is a no-op.

        Notes
        -----
        Prefer the ``with`` statement over explicit ``close()`` calls to
        guarantee cleanup even when exceptions occur.
        """
        if self.closed or self._is_closed:
            return
        mode = self._mode
        if mode in ("wb", "ab", "r+b", "xb"):
            self._fnode._rw_lock.release_write()
        else:
            self._fnode._rw_lock.release_read()
        super().close()
        self._is_closed = True

    def __enter__(self) -> MemoryFileHandle:
        return self

    def __exit__(self, *args: object) -> None:
        self.close()

    def __del__(self) -> None:
        """Fallback cleanup: emit ``ResourceWarning`` and close if not already closed.

        This is a **last-resort safety net**.  It is invoked by the garbage
        collector and its timing is not guaranteed (especially outside CPython
        or when circular references exist).

        Do not rely on ``__del__`` for deterministic lock release.  Always use
        the ``with`` statement.
        """
        if not getattr(self, "_is_closed", True) and not getattr(self, "closed", True):
            warnings.warn(
                "MFS MemoryFileHandle was not closed properly. "
                "Always use 'with mfs.open(...) as f:' to ensure cleanup.",
                ResourceWarning,
                stacklevel=1,
            )
            try:
                self.close()
            except Exception:
                pass
