"""High-level Sandbox wrappers for the Flash Sandbox SDK.

This module provides :class:`Sandbox` and :class:`AsyncSandbox`, convenient
object-oriented wrappers around a single sandbox instance.  Instead of passing
``sandbox_id`` to every client method, callers create a :class:`Sandbox` (or
:class:`AsyncSandbox`) and invoke methods directly on it.

Both classes are transport-agnostic: they accept any client that implements
the expected interface (``HTTPClient``, ``AsyncHTTPClient``, or
``SandboxClient``).

Example (sync)::

    from flash_sandbox import HTTPClient, Sandbox

    client = HTTPClient(host="localhost", port=8080)
    sandbox = Sandbox.create(
        client,
        type="docker",
        image="alpine:latest",
        command=["tail", "-f", "/dev/null"],
    )
    print(sandbox.get_status())
    result = sandbox.exec_command(["echo", "hello"])
    print(result.stdout)
    sandbox.stop()

Example (async)::

    from flash_sandbox import AsyncHTTPClient, AsyncSandbox

    async with AsyncHTTPClient(host="localhost") as client:
        sandbox = await AsyncSandbox.create(
            client,
            type="docker",
            image="alpine:latest",
        )
        status = await sandbox.get_status()
        info = await sandbox.get_platform_info()
        await sandbox.stop()
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Union

from .http_client import Command


class Sandbox:
    """High-level wrapper around a single sandbox instance (synchronous).

    This class delegates to an underlying client (``HTTPClient`` or
    ``SandboxClient``) but binds the ``sandbox_id`` so that callers
    never need to pass it explicitly.

    Parameters
    ----------
    sandbox_id:
        The unique identifier of the sandbox.
    client:
        A synchronous sandbox client instance (e.g. ``HTTPClient`` or
        ``SandboxClient``).
    """

    def __init__(self, sandbox_id: str, client: Any) -> None:
        self.id: str = sandbox_id
        self._client = client

    # ------------------------------------------------------------------ #
    # Factory
    # ------------------------------------------------------------------ #

    @classmethod
    def create(
        cls,
        client: Any,
        type: str,
        image: str,
        command: Optional[List[str]] = None,
        memory_mb: int = 512,
        cpu_cores: float = 1.0,
        **kwargs: Any,
    ) -> "Sandbox":
        """Create a new sandbox and return a :class:`Sandbox` wrapping it.

        All arguments (beyond *client*) are forwarded to the client's
        ``start_sandbox`` method.

        Parameters
        ----------
        client:
            A synchronous sandbox client.
        type:
            Sandbox backend (e.g. ``"docker"``).
        image:
            Container / rootfs image name.
        command:
            Entrypoint command.
        memory_mb:
            Memory limit in megabytes.
        cpu_cores:
            CPU cores limit.
        **kwargs:
            Additional keyword arguments forwarded to ``start_sandbox``
            (e.g. ``kernel_image``, ``snapshot_path``).

        Returns
        -------
        Sandbox
            A new :class:`Sandbox` bound to the created sandbox.
        """
        result = client.start_sandbox(
            type=type,
            image=image,
            command=command,
            memory_mb=memory_mb,
            cpu_cores=cpu_cores,
            **kwargs,
        )
        # HTTP clients return a Sandbox directly; gRPC returns a plain ID.
        if isinstance(result, cls):
            return result
        return cls(result, client)

    # ------------------------------------------------------------------ #
    # Sandbox operations
    # ------------------------------------------------------------------ #

    def get_status(self) -> str:
        """Return the current status of the sandbox (e.g. ``"running"``)."""
        return self._client.get_status(self.id)

    def exec_command(self, command: Command) -> Any:
        """Execute a command inside the sandbox.

        Parameters
        ----------
        command:
            The command to execute.  Accepts a plain string
            (``"ls -al"``), a single-element list containing a shell
            expression (``["ls -al | head"]``), or a traditional argv
            list (``["ls", "-al"]``).  Strings and single-element lists
            that contain whitespace or shell meta-characters are
            automatically wrapped in ``["sh", "-c", ...]``.

        Returns
        -------
        ExecResult or equivalent
            An object with ``stdout``, ``stderr``, and ``exit_code``.
        """
        return self._client.exec_command(self.id, command)

    def run_python(self, code: str) -> str:
        """Execute a Python code snippet inside the sandbox.

        Parameters
        ----------
        code:
            The Python source code to execute.

        Returns
        -------
        str
            The standard output produced by the code.
        """
        return self._client.run_python(self.id, code)

    def get_platform_info(self) -> Dict[str, Any]:
        """Get platform information from the sandbox.

        Returns
        -------
        dict
            A dictionary containing platform info (e.g. keys like
            ``"system"``, ``"node"``, ``"release"``, ``"version"``,
            ``"machine"``, ``"processor"``).
        """
        return self._client.get_platform_info(self.id)

    def get_metrics(self) -> Any:
        """Return point-in-time resource-usage metrics.

        Returns
        -------
        MetricsResult or equivalent
            Dataclass with memory, CPU, network, and block-I/O counters.
        """
        return self._client.get_metrics(self.id)

    def read_file(self, path: str) -> bytes:
        """Read a file from the sandbox.

        Parameters
        ----------
        path:
            The path of the file to read.

        Returns
        -------
        bytes
            The contents of the file.
        """
        return self._client.read_file(self.id, path)

    def write_file(self, path: str, content: bytes) -> None:
        """Write a file to the sandbox.

        Parameters
        ----------
        path:
            The path of the file to write.
        content:
            The file content as bytes.
        """
        self._client.write_file(self.id, path, content)

    def snapshot(self) -> Dict[str, str]:
        """Create a snapshot of the sandbox.

        Returns
        -------
        dict
            ``{"snapshot_path": "...", "mem_file_path": "..."}``.
        """
        return self._client.snapshot_sandbox(self.id)

    def resume(self) -> None:
        """Resume the sandbox from a paused / snapshotted state."""
        self._client.resume_sandbox(self.id)

    def stop(self, cleanup: bool = True) -> None:
        """Stop the sandbox.

        Parameters
        ----------
        cleanup:
            When ``True`` (the default), backing resources (container, VM,
            temporary files) are removed.  When ``False`` the sandbox
            process is stopped but resources are left in place for
            inspection or later resumption.
        """
        self._client.stop_sandbox(self.id, cleanup=cleanup)

    # ------------------------------------------------------------------ #
    # Context manager
    # ------------------------------------------------------------------ #

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

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        self.stop(cleanup=True)

    # ------------------------------------------------------------------ #
    # Representation
    # ------------------------------------------------------------------ #

    def __repr__(self) -> str:
        return f"Sandbox(id={self.id!r})"


class AsyncSandbox:
    """High-level wrapper around a single sandbox instance (asynchronous).

    This is the ``async`` counterpart of :class:`Sandbox`.  It delegates
    to an asynchronous client (``AsyncHTTPClient``) and all I/O methods
    are coroutines.

    Parameters
    ----------
    sandbox_id:
        The unique identifier of the sandbox.
    client:
        An asynchronous sandbox client instance (e.g. ``AsyncHTTPClient``).
    """

    def __init__(self, sandbox_id: str, client: Any) -> None:
        self.id: str = sandbox_id
        self._client = client

    # ------------------------------------------------------------------ #
    # Factory
    # ------------------------------------------------------------------ #

    @classmethod
    async def create(
        cls,
        client: Any,
        type: str,
        image: str,
        command: Optional[List[str]] = None,
        memory_mb: int = 512,
        cpu_cores: float = 1.0,
        **kwargs: Any,
    ) -> "AsyncSandbox":
        """Create a new sandbox and return an :class:`AsyncSandbox` wrapping it.

        All arguments (beyond *client*) are forwarded to the client's
        ``start_sandbox`` coroutine.

        Parameters
        ----------
        client:
            An asynchronous sandbox client.
        type:
            Sandbox backend (e.g. ``"docker"``).
        image:
            Container / rootfs image name.
        command:
            Entrypoint command.
        memory_mb:
            Memory limit in megabytes.
        cpu_cores:
            CPU cores limit.
        **kwargs:
            Additional keyword arguments forwarded to ``start_sandbox``
            (e.g. ``kernel_image``, ``snapshot_path``).

        Returns
        -------
        AsyncSandbox
            A new :class:`AsyncSandbox` bound to the created sandbox.
        """
        result = await client.start_sandbox(
            type=type,
            image=image,
            command=command,
            memory_mb=memory_mb,
            cpu_cores=cpu_cores,
            **kwargs,
        )
        # HTTP clients return an AsyncSandbox directly; gRPC returns a plain ID.
        if isinstance(result, cls):
            return result
        return cls(result, client)

    # ------------------------------------------------------------------ #
    # Sandbox operations
    # ------------------------------------------------------------------ #

    async def get_status(self) -> str:
        """Return the current status of the sandbox (e.g. ``"running"``)."""
        return await self._client.get_status(self.id)

    async def exec_command(self, command: Command) -> Any:
        """Execute a command inside the sandbox.

        Parameters
        ----------
        command:
            The command to execute.  Accepts a plain string
            (``"ls -al"``), a single-element list containing a shell
            expression (``["ls -al | head"]``), or a traditional argv
            list (``["ls", "-al"]``).  Strings and single-element lists
            that contain whitespace or shell meta-characters are
            automatically wrapped in ``["sh", "-c", ...]``.

        Returns
        -------
        ExecResult or equivalent
            An object with ``stdout``, ``stderr``, and ``exit_code``.
        """
        return await self._client.exec_command(self.id, command)

    async def run_python(self, code: str) -> str:
        """Execute a Python code snippet inside the sandbox.

        Parameters
        ----------
        code:
            The Python source code to execute.

        Returns
        -------
        str
            The standard output produced by the code.
        """
        return await self._client.run_python(self.id, code)

    async def get_platform_info(self) -> Dict[str, Any]:
        """Get platform information from the sandbox.

        Returns
        -------
        dict
            A dictionary containing platform info (e.g. keys like
            ``"system"``, ``"node"``, ``"release"``, ``"version"``,
            ``"machine"``, ``"processor"``).
        """
        return await self._client.get_platform_info(self.id)

    async def get_metrics(self) -> Any:
        """Return point-in-time resource-usage metrics.

        Returns
        -------
        MetricsResult or equivalent
            Dataclass with memory, CPU, network, and block-I/O counters.
        """
        return await self._client.get_metrics(self.id)

    async def read_file(self, path: str) -> bytes:
        """Read a file from the sandbox.

        Parameters
        ----------
        path:
            The path of the file to read.

        Returns
        -------
        bytes
            The contents of the file.
        """
        return await self._client.read_file(self.id, path)

    async def write_file(self, path: str, content: bytes) -> None:
        """Write a file to the sandbox.

        Parameters
        ----------
        path:
            The path of the file to write.
        content:
            The file content as bytes.
        """
        await self._client.write_file(self.id, path, content)

    async def snapshot(self) -> Dict[str, str]:
        """Create a snapshot of the sandbox.

        Returns
        -------
        dict
            ``{"snapshot_path": "...", "mem_file_path": "..."}``.
        """
        return await self._client.snapshot_sandbox(self.id)

    async def resume(self) -> None:
        """Resume the sandbox from a paused / snapshotted state."""
        await self._client.resume_sandbox(self.id)

    async def stop(self, cleanup: bool = True) -> None:
        """Stop the sandbox.

        Parameters
        ----------
        cleanup:
            When ``True`` (the default), backing resources (container, VM,
            temporary files) are removed.  When ``False`` the sandbox
            process is stopped but resources are left in place for
            inspection or later resumption.
        """
        await self._client.stop_sandbox(self.id, cleanup=cleanup)

    # ------------------------------------------------------------------ #
    # Async context manager
    # ------------------------------------------------------------------ #

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

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        await self.stop(cleanup=True)

    # ------------------------------------------------------------------ #
    # Representation
    # ------------------------------------------------------------------ #

    def __repr__(self) -> str:
        return f"AsyncSandbox(id={self.id!r})"
