"""
Module: repl.client
Description: HTTP client wrapper for REPL communication with the local sandbox.

Implements:
    - docs/cli/reference.md#interactive-repl
    - docs/cli/reference.md#huitzo-pack-dev

See Also:
    - docs/cli/reference.md#endpoints-available-via-proxy
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import httpx

from huitzo_cli.errors import (
    AuthenticationError,
    CommandNotFoundError,
    CommandValidationError,
    HuitzoCliError,
    NetworkError,
    SandboxCommandError,
    ServerError,
)


def _check_response(response: httpx.Response) -> None:
    """Check HTTP response and raise typed exceptions for error status codes.

    Replaces response.raise_for_status() to preserve error detail from the
    sandbox server instead of discarding it.
    """
    if response.is_success:
        return

    # Try to extract detail from JSON body
    detail = ""
    try:
        body = response.json()
        detail = body.get("detail", "")
    except Exception:
        detail = response.text or ""

    status = response.status_code

    if status == 422:
        raise CommandValidationError(detail or "Validation failed")
    elif status == 404:
        raise CommandNotFoundError(detail or "Not found")
    elif status == 401:
        raise AuthenticationError(
            "Authentication failed",
            user_message="Authentication failed. Run: huitzo login",
        )
    elif status >= 500:
        raise ServerError(
            f"Server error {status}",
            user_message=detail or "Server error. Try again later.",
        )
    else:
        raise HuitzoCliError(
            f"Request failed with status {status}",
            user_message=detail or f"Request failed (HTTP {status})",
        )


class REPLClient:
    """HTTP client for communicating with the local sandbox server.

    Wraps httpx.AsyncClient to provide typed methods for listing commands,
    executing commands, and checking health. Accepts a verify parameter
    for TLS certificate validation (CA cert path from sandbox).
    """

    def __init__(
        self,
        base_url: str,
        auth_token: str | None = None,
        verify: str | bool = True,
    ) -> None:
        """Initialize the REPL client.

        Args:
            base_url: Base URL of the sandbox server (e.g. https://localhost:8080)
            auth_token: Bearer token for sandbox authentication
            verify: TLS verification — True, False, or path to CA cert file
        """
        self._base_url = base_url.rstrip("/")
        self._auth_token = auth_token
        self._verify = verify
        self._client: httpx.AsyncClient | None = None

    async def _ensure_client(self) -> httpx.AsyncClient:
        """Lazily create the async HTTP client."""
        if self._client is None:
            headers: dict[str, str] = {}
            if self._auth_token:
                headers["Authorization"] = f"Bearer {self._auth_token}"
            self._client = httpx.AsyncClient(
                base_url=self._base_url,
                headers=headers,
                verify=self._verify,
                timeout=60.0,
            )
        return self._client

    async def __aenter__(self) -> REPLClient:
        """Enter async context manager."""
        return self

    async def __aexit__(self, *exc: object) -> None:
        """Exit async context manager."""
        await self.close()

    async def list_commands(self) -> dict[str, Any]:
        """Fetch available commands from the sandbox.

        Returns:
            Dict with "commands" key containing list of command metadata.

        Raises:
            CommandNotFoundError: On 404
            AuthenticationError: On 401
            ServerError: On 5xx
            HuitzoCliError: On other errors
        """
        client = await self._ensure_client()
        try:
            response = await client.get("/commands")
        except httpx.ConnectError as e:
            raise NetworkError(
                str(e), user_message="Cannot connect to sandbox. Is it running?"
            ) from e
        except httpx.TimeoutException as e:
            raise NetworkError(str(e), user_message="Sandbox request timed out.") from e
        _check_response(response)
        result: dict[str, Any] = response.json()
        return result

    async def execute_command(self, name: str, args: dict[str, Any]) -> dict[str, Any]:
        """Execute a pack command via the sandbox.

        Args:
            name: Command name (short name, not namespaced)
            args: Command arguments as a dict

        Returns:
            Command result wrapped in {"data": ...}

        Raises:
            SandboxCommandError: On structured error response from sandbox
            CommandValidationError: On 422
            CommandNotFoundError: On 404
            AuthenticationError: On 401
            ServerError: On 5xx
            HuitzoCliError: On other errors
        """
        client = await self._ensure_client()
        try:
            response = await client.post(f"/commands/{name}", json={"args": args})
        except httpx.ConnectError as e:
            raise NetworkError(
                str(e), user_message="Cannot connect to sandbox. Is it running?"
            ) from e
        except httpx.TimeoutException as e:
            raise NetworkError(str(e), user_message="Sandbox request timed out.") from e
        # Try to parse structured error from sandbox before generic _check_response
        if not response.is_success:
            try:
                body = response.json()
                if "error" in body and "message" in body:
                    raise SandboxCommandError(
                        error_type=body["error"],
                        message=body["message"],
                        command=body.get("command", name),
                        code=body.get("code"),
                    )
            except (ValueError, KeyError):
                pass
        _check_response(response)
        result: dict[str, Any] = response.json()
        return result

    async def describe_command(self, name: str) -> dict[str, Any] | None:
        """Get metadata for a specific command.

        Args:
            name: Command name to describe

        Returns:
            Command metadata dict, or None if not found
        """
        data = await self.list_commands()
        commands: list[dict[str, Any]] = data.get("commands", [])
        for cmd in commands:
            if cmd.get("name") == name:
                return cmd
        return None

    async def health(self) -> bool:
        """Check if the sandbox is healthy.

        Returns:
            True if the sandbox responds with status ok
        """
        try:
            client = await self._ensure_client()
            response = await client.get("/health")
            return response.status_code == 200
        except httpx.HTTPError:
            return False

    async def upload_file(self, path: Path) -> dict[str, Any]:
        """Upload a local file to the sandbox.

        Args:
            path: Local filesystem path to the file

        Returns:
            Dict with sandbox path, size, and original_name

        Raises:
            FileNotFoundError: If the local file does not exist
            CommandValidationError: On 422
            AuthenticationError: On 401
            ServerError: On 5xx
        """
        if not path.exists():
            raise FileNotFoundError(f"File not found: {path}")
        if not path.is_file():
            raise FileNotFoundError(f"Not a file: {path}")

        client = await self._ensure_client()
        with open(path, "rb") as f:
            response = await client.post(
                "/files",
                files={"file": (path.name, f)},
            )
        _check_response(response)
        result: dict[str, Any] = response.json()
        return result

    async def list_files(self) -> dict[str, Any]:
        """List files uploaded to the sandbox.

        Returns:
            Dict with "files" key containing list of file metadata

        Raises:
            AuthenticationError: On 401
            ServerError: On 5xx
        """
        client = await self._ensure_client()
        response = await client.get("/files")
        _check_response(response)
        result: dict[str, Any] = response.json()
        return result

    async def clear_files(self) -> dict[str, Any]:
        """Delete all uploaded files from the sandbox.

        Returns:
            Dict with "deleted" key containing count of deleted files

        Raises:
            AuthenticationError: On 401
            ServerError: On 5xx
        """
        client = await self._ensure_client()
        response = await client.delete("/files")
        _check_response(response)
        result: dict[str, Any] = response.json()
        return result

    async def close(self) -> None:
        """Close the underlying HTTP client."""
        if self._client is not None:
            await self._client.aclose()
            self._client = None
