# ClawTex — Governed Agent with Genetic Memory
# Built by DeVere Cooley / AN2B LLC · an2b.com
# BB4C — Breath Before Code

"""
MandelDB HTTP client — semantic memory backend for ClawTex agents.

Provides:
  recall(query, top_k=5)     → POST /v2/recall     — fetch relevant memories
  ingest(content, metadata)  → POST /v2/memory      — store new memory
  dream(namespace)           → POST /v2/brain/dream — trigger consolidation

All methods are async. Retries on 429 (backoff) and 5xx (up to 3 attempts).
"""

from __future__ import annotations

import asyncio
import logging
import os
from typing import Any

import httpx

logger = logging.getLogger(__name__)

_RETRY_STATUSES = {429, 500, 502, 503, 504}
_MAX_RETRIES = 3
_BASE_BACKOFF = 1.0  # seconds


class MandelDBClient:
    """Async client for the MandelDB memory API."""

    def __init__(
        self,
        base_url: str | None = None,
        api_key: str | None = None,
        timeout: float = 30.0,
    ) -> None:
        self.base_url = (base_url or os.environ["MANDELDB_URL"]).rstrip("/")
        self.api_key = api_key or os.environ["MANDELDB_API_KEY"]
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers={
                "X-API-Key": self.api_key,
                "Content-Type": "application/json",
            },
            timeout=timeout,
        )

    async def _request(
        self,
        method: str,
        path: str,
        payload: dict[str, Any],
    ) -> dict[str, Any]:
        """Execute an HTTP request with exponential-backoff retry."""
        last_exc: Exception | None = None
        for attempt in range(_MAX_RETRIES):
            try:
                response = await self._client.request(method, path, json=payload)
                if response.status_code in _RETRY_STATUSES:
                    wait = _BASE_BACKOFF * (2 ** attempt)
                    # Honour Retry-After if present
                    retry_after = response.headers.get("Retry-After")
                    if retry_after:
                        try:
                            wait = float(retry_after)
                        except ValueError:
                            pass
                    logger.warning(
                        "MandelDB %s %s → %d, retrying in %.1fs (attempt %d/%d)",
                        method,
                        path,
                        response.status_code,
                        wait,
                        attempt + 1,
                        _MAX_RETRIES,
                    )
                    await asyncio.sleep(wait)
                    continue
                response.raise_for_status()
                return response.json()
            except httpx.RequestError as exc:
                last_exc = exc
                wait = _BASE_BACKOFF * (2 ** attempt)
                logger.warning(
                    "MandelDB request error: %s — retrying in %.1fs", exc, wait
                )
                await asyncio.sleep(wait)

        raise RuntimeError(
            f"MandelDB request failed after {_MAX_RETRIES} attempts"
        ) from last_exc

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def recall(
        self,
        query: str,
        top_k: int = 5,
        namespace: str | None = None,
    ) -> list[dict[str, Any]]:
        """
        Retrieve the top-k semantically similar memories for *query*.

        Returns a list of memory objects (content, score, metadata).
        """
        payload: dict[str, Any] = {"query": query, "top_k": top_k}
        if namespace:
            payload["namespace"] = namespace
        result = await self._request("POST", "/v2/recall", payload)
        return result.get("memories", result if isinstance(result, list) else [])

    async def ingest(
        self,
        content: str,
        metadata: dict[str, Any] | None = None,
        namespace: str | None = None,
    ) -> dict[str, Any]:
        """
        Store *content* as a new memory entry.

        *metadata* can include agent_name, session_id, timestamp, source, etc.
        Returns the created memory record.
        """
        payload: dict[str, Any] = {
            "content": content,
            "metadata": metadata or {},
        }
        if namespace:
            payload["namespace"] = namespace
        return await self._request("POST", "/v2/memory", payload)

    async def dream(self, namespace: str) -> dict[str, Any]:
        """
        Trigger memory consolidation (dreaming) for the given namespace.

        This allows MandelDB to cluster, summarise, and compress memories
        — analogous to REM sleep in biological memory systems.
        Returns the dream job status.
        """
        return await self._request(
            "POST", "/v2/brain/dream", {"namespace": namespace}
        )

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

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

    async def __aexit__(self, *_: Any) -> None:
        await self.close()
