"""
hollow.client — synchronous HollowClient backed by httpx.
"""

from __future__ import annotations

import os
from typing import Any
from urllib.parse import quote

import httpx

from hollow.types import Action, PerceiveResult, Tier


class HollowClient:
    """Synchronous client for the Hollow API.

    Args:
        endpoint: Base URL of your Hollow deployment.
                  Reads ``HOLLOW_ENDPOINT`` from the environment if set.
                  Defaults to the public hosted instance.

    Example::

        from hollow import HollowClient

        client = HollowClient()
        page = client.perceive("https://news.ycombinator.com")
        print(page.gdg_map)
    """

    def __init__(
        self,
        endpoint: str = "https://hollow-tan-omega.vercel.app",
    ) -> None:
        self.endpoint = os.environ.get("HOLLOW_ENDPOINT", endpoint).rstrip("/")
        self._http = httpx.Client(timeout=90.0)

    # ── Internal ──────────────────────────────────────────────────────────────

    def _post(self, path: str, body: dict) -> dict:
        try:
            res = self._http.post(
                f"{self.endpoint}{path}",
                json=body,
                headers={"Content-Type": "application/json"},
            )
        except httpx.RequestError as exc:
            raise ConnectionError(f"Hollow network error: {exc}") from exc

        return self._check(res)

    def _get(self, path: str) -> dict:
        try:
            res = self._http.get(f"{self.endpoint}{path}")
        except httpx.RequestError as exc:
            raise ConnectionError(f"Hollow network error: {exc}") from exc

        return self._check(res)

    def _delete(self, path: str) -> dict:
        try:
            res = self._http.delete(f"{self.endpoint}{path}")
        except httpx.RequestError as exc:
            raise ConnectionError(f"Hollow network error: {exc}") from exc

        return self._check(res)

    @staticmethod
    def _check(res: httpx.Response) -> dict:
        if res.is_success:
            try:
                return res.json()
            except Exception:
                return {}

        # Try to extract structured error from body
        body: dict[str, Any] = {}
        try:
            body = res.json()
        except Exception:
            pass

        tier_hint = f" (tier: {body['tier']})" if "tier" in body else ""
        message = (
            body.get("message")
            or body.get("error")
            or res.reason_phrase
            or "unknown error"
        )
        raise ValueError(
            f"Hollow API error {res.status_code}{tier_hint}: {message}"
        )

    @staticmethod
    def _parse_result(data: dict) -> PerceiveResult:
        return PerceiveResult(
            session_id=data["sessionId"],
            gdg_map=data.get("gdgMap", ""),
            confidence=float(data.get("confidence") or 0.0),
            tier=data.get("tier", "partial"),  # type: ignore[arg-type]
            js_errors=data.get("jsErrors") or [],
        )

    # ── Public API ────────────────────────────────────────────────────────────

    def perceive(
        self,
        url: str,
        session_id: str | None = None,
        state_id: str | None = None,
    ) -> PerceiveResult:
        """Load a URL in Hollow's serverless browser.

        Returns a GDG Spatial map — a structured spatial tree with
        actionable element IDs an AI agent can reason about and act on.

        Args:
            url:        The URL to load.
            session_id: Optional — resume an existing session.
            state_id:   Optional — Hydra state provider hook.

        Returns:
            PerceiveResult with session_id, gdg_map, confidence, tier.

        Raises:
            ValueError: On non-2xx response from Hollow.
            ConnectionError: On network-level failure.
        """
        body: dict[str, Any] = {"url": url}
        if session_id is not None:
            body["sessionId"] = session_id
        if state_id is not None:
            body["stateId"] = state_id

        data = self._post("/api/perceive", body)
        return self._parse_result(data)

    def act(
        self,
        session_id: str,
        action: Action,
        intervention: str | None = None,
    ) -> PerceiveResult:
        """Interact with the current page in a Hollow session.

        Element IDs (action.element_id) come from the gdg_map returned
        by perceive().

        Args:
            session_id:   The session to act on.
            action:       The action to perform.
            intervention: Optional human instruction injected into context.

        Returns:
            Updated PerceiveResult with the new gdg_map after the action.

        Raises:
            ValueError: On non-2xx response from Hollow.
            ConnectionError: On network-level failure.
        """
        body: dict[str, Any] = {
            "sessionId": session_id,
            "action": action.to_dict(),
        }
        if intervention is not None:
            body["intervention"] = intervention

        data = self._post("/api/act", body)
        return self._parse_result(data)

    def get_session(self, session_id: str) -> dict:
        """Get the current state of an active session without acting.

        Returns:
            Raw session dict with sessionId, url, tier, confidence, updatedAt.

        Raises:
            ValueError: Session not found or expired.
            ConnectionError: On network-level failure.
        """
        return self._get(f"/api/session/{quote(session_id, safe='')}")

    def close_session(self, session_id: str) -> None:
        """Close a Hollow session and free its Redis state.

        Safe to call multiple times (idempotent).

        Raises:
            ValueError: On non-2xx response from Hollow.
            ConnectionError: On network-level failure.
        """
        self._delete(f"/api/session/{quote(session_id, safe='')}")

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

    def __exit__(self, *_: Any) -> None:
        self._http.close()

    def close(self) -> None:
        """Close the underlying httpx client."""
        self._http.close()
