# adapters/vercel.py

from __future__ import annotations
from dataclasses import dataclass
from typing import Any

import httpx

from ..core.types import SandboxAdapter, ExecResult, ExecOpts


def vercel(sandbox: Any) -> SandboxAdapter:
    """
    Adapter for Vercel Sandbox.

    Accepts either a ``VercelSandbox`` instance (built-in HTTP client)
    or any object with ``run_command``, ``write_files``, ``read_file``,
    and ``stop`` methods.

    Usage::

        from agentsh_secure_sandbox.adapters.vercel import VercelSandbox, vercel

        sb = await VercelSandbox.create(token=..., project_id=..., team_id=...)
        adapter = vercel(sb)
    """
    return _VercelAdapter(sandbox)


# ─── Built-in HTTP client ─────────────────────────────────


@dataclass
class _RunResult:
    stdout: str
    stderr: str
    exit_code: int


class VercelSandbox:
    """HTTP client for the Vercel Sandbox REST API.

    Provides the interface expected by the ``vercel()`` adapter so that
    Python users don't need to write their own HTTP wrapper.
    """

    def __init__(
        self,
        *,
        token: str,
        project_id: str,
        team_id: str,
        runtime: str = "node24",
        timeout: int = 600_000,
        base_url: str = "https://api.vercel.com",
    ) -> None:
        self._token = token
        self._project_id = project_id
        self._team_id = team_id
        self._runtime = runtime
        self._timeout = timeout
        self._base = base_url
        self._client = httpx.AsyncClient(timeout=120)
        self._sandbox_id: str | None = None

    @classmethod
    async def create(
        cls,
        *,
        token: str,
        project_id: str,
        team_id: str,
        runtime: str = "node24",
        timeout: int = 600_000,
    ) -> VercelSandbox:
        """Create and provision a new Vercel sandbox."""
        sb = cls(
            token=token,
            project_id=project_id,
            team_id=team_id,
            runtime=runtime,
            timeout=timeout,
        )
        await sb._provision()
        return sb

    async def _provision(self) -> None:
        res = await self._client.post(
            f"{self._base}/v1/sandbox",
            headers={"Authorization": f"Bearer {self._token}"},
            json={
                "runtime": self._runtime,
                "timeout": self._timeout,
                "projectId": self._project_id,
                "teamId": self._team_id,
            },
        )
        res.raise_for_status()
        data = res.json()
        self._sandbox_id = data["sandboxId"]

    async def run_command(
        self,
        cmd: str,
        args: list[str] | None = None,
        *,
        cwd: str | None = None,
        sudo: bool = False,
        detached: bool = False,
    ) -> _RunResult:
        body: dict[str, Any] = {"cmd": cmd, "args": args or []}
        if cwd:
            body["cwd"] = cwd
        if sudo:
            body["sudo"] = True
        if detached:
            body["detached"] = True
        res = await self._client.post(
            f"{self._base}/v1/sandbox/{self._sandbox_id}/exec",
            headers={"Authorization": f"Bearer {self._token}"},
            json=body,
        )
        res.raise_for_status()
        data = res.json()
        return _RunResult(
            stdout=data.get("stdout", ""),
            stderr=data.get("stderr", ""),
            exit_code=data.get("exitCode", 1),
        )

    async def write_files(self, files: list[dict[str, Any]]) -> None:
        import base64 as _b64

        for f in files:
            content = f["content"]
            if isinstance(content, bytes):
                content = _b64.b64encode(content).decode("ascii")
                encoding = "base64"
            else:
                encoding = "utf-8"
            res = await self._client.post(
                f"{self._base}/v1/sandbox/{self._sandbox_id}/files",
                headers={"Authorization": f"Bearer {self._token}"},
                json={"path": f["path"], "content": content, "encoding": encoding},
            )
            res.raise_for_status()

    async def read_file(self, path: str) -> str:
        res = await self._client.get(
            f"{self._base}/v1/sandbox/{self._sandbox_id}/files",
            headers={"Authorization": f"Bearer {self._token}"},
            params={"path": path},
        )
        res.raise_for_status()
        return res.text

    async def stop(self) -> None:
        try:
            if self._sandbox_id:
                res = await self._client.delete(
                    f"{self._base}/v1/sandbox/{self._sandbox_id}",
                    headers={"Authorization": f"Bearer {self._token}"},
                )
                res.raise_for_status()
        finally:
            await self._client.aclose()


# ─── Adapter ──────────────────────────────────────────────


class _VercelAdapter:
    def __init__(self, sandbox: Any) -> None:
        self._sb = sandbox

    async def exec(
        self,
        cmd: str,
        args: list[str] | None = None,
        opts: ExecOpts | None = None,
    ) -> ExecResult:
        opts = opts or ExecOpts()
        params: dict[str, Any] = {"cmd": cmd, "args": args or []}
        if opts.cwd:
            params["cwd"] = opts.cwd
        if opts.sudo:
            params["sudo"] = True
        if opts.detached:
            params["detached"] = True

        result = await self._sb.run_command(**params)

        if opts.detached:
            return ExecResult(stdout="", stderr="", exit_code=0)

        stdout = result.stdout
        if callable(stdout):
            stdout = await stdout()
        stderr = result.stderr
        if callable(stderr):
            stderr = await stderr()

        return ExecResult(
            stdout=stdout or "",
            stderr=stderr or "",
            exit_code=result.exit_code,
        )

    async def write_file(self, path: str, content: str | bytes, *, sudo: bool = False) -> None:
        # sudo is not supported by Vercel's file API — silently ignored.
        data = content if isinstance(content, bytes) else content.encode()
        await self._sb.write_files([{"path": path, "content": data}])

    async def read_file(self, path: str) -> str:
        stream = await self._sb.read_file(path)
        if hasattr(stream, "__aiter__"):
            chunks: list[bytes] = []
            async for chunk in stream:
                chunks.append(chunk if isinstance(chunk, bytes) else chunk.encode())
            return b"".join(chunks).decode("utf-8", errors="replace")
        if isinstance(stream, bytes):
            return stream.decode("utf-8", errors="replace")
        return stream if isinstance(stream, str) else str(stream)

    async def stop(self) -> None:
        await self._sb.stop()

    async def file_exists(self, path: str) -> bool:
        result = await self.exec("test", ["-f", path])
        return result.exit_code == 0
