"""
client.py
---------
Python SDK client for HIEROMEM API.

Provides a simple interface for interacting with the HIEROMEM memory system.

Author: Sai Rishi Kiran Mannava
"""

from __future__ import annotations

from typing import Any, Optional

import requests


class HieromemError(RuntimeError):
    """Raised when the HIEROMEM API returns an error response."""


class HieromemClient:
    """Client for interacting with the HIEROMEM API.

    The client is intentionally minimal and depends only on ``requests`` so it
    can be embedded in automation, Jupyter notebooks, or production services
    without extra runtime baggage.
    """

    def __init__(
        self,
        base_url: str,
        api_key: Optional[str] = None,
        oauth_token: Optional[str] = None,
        timeout: float = 10.0,
        session: Optional[requests.Session] = None,
        user_agent: str = "hieromem-python-sdk/0.1.0",
    ):
        """
        Initialize the HIEROMEM client.

        Args:
            base_url: Base URL of the HIEROMEM API (e.g., "http://localhost:8000").
            api_key: API key for authentication.
            oauth_token: OAuth bearer token for authentication.
            timeout: Default request timeout in seconds.
            session: Optional requests session for connection pooling.
            user_agent: Optional custom user-agent for observability.
        """

        if not base_url:
            raise ValueError("base_url is required")
        if not (api_key or oauth_token):
            raise ValueError("either api_key or oauth_token must be provided")

        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.oauth_token = oauth_token
        self.timeout = timeout
        self.session = session or requests.Session()
        self.user_agent = user_agent

    def _headers(self, extra: Optional[dict[str, str]] = None) -> dict[str, str]:
        """Build request headers including authentication."""

        headers: dict[str, str] = {
            "Content-Type": "application/json",
            "User-Agent": self.user_agent,
        }
        if self.oauth_token:
            headers["Authorization"] = f"Bearer {self.oauth_token}"
        elif self.api_key:
            headers["X-API-Key"] = self.api_key

        if extra:
            headers.update(extra)
        return headers

    def close(self) -> None:
        """Close the underlying HTTP session."""

        self.session.close()

    def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
        """Perform an HTTP request with shared authentication and error handling."""

        url = f"{self.base_url}{path}"
        headers = kwargs.pop("headers", {})
        merged_headers = self._headers(headers)

        try:
            response = self.session.request(
                method,
                url,
                headers=merged_headers,
                timeout=self.timeout,
                **kwargs,
            )
        except requests.RequestException as exc:  # pragma: no cover - network/system errors
            raise HieromemError(f"Request to {url} failed: {exc}") from exc

        if not response.ok:
            detail: Any
            try:
                detail = response.json()
            except Exception:  # pragma: no cover - best effort diagnostic path
                detail = response.text
            raise HieromemError(
                f"Request to {url} failed with {response.status_code}: {detail}"
            )

        if "application/json" in response.headers.get("content-type", ""):
            return response.json()
        return {"status": response.text}

    def remember(
        self,
        text: str,
        namespace: Optional[str] = None,
        meta: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        """
        Store a new memory.

        Args:
            text: Text content to remember
            namespace: Optional namespace for multi-tenancy
            meta: Optional metadata dictionary

        Returns:
            Response from API with episode_id
        """
        payload = {"text": text}
        if namespace:
            payload["namespace"] = namespace
        if meta:
            payload["meta"] = meta

        return self._request("POST", "/memory/remember", json=payload)

    def recall(
        self,
        query: str,
        k: int = 3,
        namespace: Optional[str] = None,
    ) -> dict[str, Any]:
        """
        Retrieve relevant memories.

        Args:
            query: Query text
            k: Number of results to return
            namespace: Optional namespace for multi-tenancy

        Returns:
            Response from API with results
        """
        payload = {"query": query, "k": k}
        if namespace:
            payload["namespace"] = namespace

        return self._request("POST", "/memory/recall", json=payload)

    def dual_recall(
        self,
        query: str,
        k: int = 3,
        namespace: Optional[str] = None,
    ) -> dict[str, Any]:
        """
        Retrieve memories using dual (vector + graph) retrieval.

        Args:
            query: Query text
            k: Number of results to return
            namespace: Optional namespace for multi-tenancy

        Returns:
            Response from API with results
        """
        payload = {"query": query, "k": k}
        if namespace:
            payload["namespace"] = namespace

        return self._request("POST", "/memory/dual-recall", json=payload)

    def symbolic_search(
        self,
        path: Optional[list[str]] = None,
        namespace: Optional[str] = None,
        limit: int = 5,
    ) -> dict[str, Any]:
        """
        Search using symbolic graph queries.

        Args:
            path: Optional path of nodes to match
            namespace: Optional namespace for multi-tenancy
            limit: Maximum number of results

        Returns:
            Response from API with graph matches
        """
        payload = {"limit": limit}
        if path:
            payload["path"] = path
        if namespace:
            payload["namespace"] = namespace

        return self._request("POST", "/memory/symbolic-search", json=payload)

    def forget(
        self,
        episode_ids: Optional[list[str]] = None,
        threshold: Optional[float] = None,
        namespace: Optional[str] = None,
    ) -> dict[str, Any]:
        """
        Forget specific episodes or prune by trust threshold.

        Args:
            episode_ids: Optional list of episode IDs to forget
            threshold: Optional trust threshold for pruning
            namespace: Optional namespace for multi-tenancy

        Returns:
            Response from API with removed episode IDs
        """
        payload = {}
        if episode_ids:
            payload["episode_ids"] = episode_ids
        if threshold is not None:
            payload["threshold"] = threshold
        if namespace:
            payload["namespace"] = namespace

        return self._request("POST", "/memory/forget", json=payload)

    def health(self) -> dict[str, Any]:
        """Lightweight health probe for readiness checks."""

        return self._request("GET", "/health")
