"""ACIS MemoryClient — primary SDK entry point (HTTP-backed).

Usage:
    from acis import MemoryClient

    client = MemoryClient(tenant_id="abc-123", base_url="http://acis-server:8000")
    session = client.create_session(user_id="user-1")
    client.add_event(session_id=session.session_id, role="user", content="Hello")
    ctx = client.get_context(session_id=session.session_id, query="Hello")
    # ctx.messages → pass directly to any LLM
"""
import logging
from dataclasses import dataclass
from typing import Optional

import httpx

from acis.exceptions import (
    ACISError,
    ACISRateLimitError,
    ACISSessionNotFoundError,
    ACISCircuitOpenError,
    ACISValidationError,
)

logger = logging.getLogger(__name__)

DEFAULT_BASE_URL = "http://localhost:8000"
API_PREFIX = "/api/v1"
DEFAULT_TIMEOUT = httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)


@dataclass
class SessionResult:
    session_id: str
    tenant_id: str
    user_id: str
    agent_id: Optional[str] = None


@dataclass
class CloseResult:
    session_id: str
    closed: bool
    close_reason: str = "user_ended"
    closed_at: Optional[str] = None
    already_closed: bool = False


@dataclass
class WriteResult:
    event_id: str
    seq: int
    created: bool


@dataclass
class ContextResult:
    messages: list
    context_payload: list
    events_injected: int
    memories_injected: int
    summary_injected: bool
    rules_injected: int
    total_tokens: int
    token_budget: int
    remaining_budget: int
    memory_ids_used: list


@dataclass
class SearchResult:
    memory_id: str
    content: str
    category: str
    importance: float
    agent_id: Optional[str] = None
    entities: Optional[dict] = None
    rrf_score: Optional[float] = None


@dataclass
class MemoryHistoryEntry:
    memory_id: str
    content: str
    category: str
    importance: float
    created_at: Optional[str] = None
    valid_to: Optional[str] = None
    superseded_by: Optional[str] = None


class MemoryClient:
    """ACIS Python SDK — single entry point for all memory operations.

    Args:
        tenant_id: Tenant UUID (required)
        user_id: Default user ID (optional, can override per-call)
        agent_id: Default agent ID for scoped operations (optional)
        base_url: ACIS API base URL (e.g. http://acis-server:8000)
        timeout: HTTP timeout configuration (optional)

    Example:
        from acis import MemoryClient

        client = MemoryClient(
            tenant_id="abc-123",
            base_url="http://10.0.1.50:8000"
        )
        session = client.create_session(user_id="user-1")
        client.add_event(session_id=session.session_id, role="user", content="Hello")
        ctx = client.get_context(session_id=session.session_id, query="Hello")
        response = your_llm(messages=ctx.messages)
    """

    def __init__(
        self,
        tenant_id: str,
        user_id: Optional[str] = None,
        agent_id: Optional[str] = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: Optional[httpx.Timeout] = None,
    ):
        if not tenant_id or not tenant_id.strip():
            raise ACISValidationError("tenant_id is required and cannot be empty")

        self.tenant_id = tenant_id.strip()
        self.user_id = user_id
        self.agent_id = agent_id
        self.base_url = base_url.rstrip("/")
        self._api_url = f"{self.base_url}{API_PREFIX}"

        self._http = httpx.Client(
            base_url=self._api_url,
            headers={"X-Tenant-Id": self.tenant_id},
            timeout=timeout or DEFAULT_TIMEOUT,
        )
        self._resolved_session_id: Optional[str] = None
        logger.info(f"[ACIS SDK] Initialized: tenant={self.tenant_id}, api={self._api_url}")

    def close(self):
        """Close the underlying HTTP client."""
        self._http.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    # ─── HTTP Helpers ─────────────────────────────────────────────

    def _request(self, method: str, path: str, **kwargs) -> dict:
        try:
            response = self._http.request(method, path, **kwargs)
        except httpx.ConnectError as e:
            raise ACISError(f"Cannot connect to ACIS at {self.base_url}: {e}", code="CONNECTION_ERROR") from e
        except httpx.TimeoutException as e:
            raise ACISError(f"Request to ACIS timed out: {e}", code="TIMEOUT") from e

        if response.status_code in (200, 201, 202):
            return response.json()

        try:
            body = response.json()
        except Exception:
            body = {"error": response.text}

        detail = body.get("detail") or body.get("error", "Unknown error")

        if response.status_code == 400:
            raise ACISValidationError(detail)
        elif response.status_code == 404:
            raise ACISSessionNotFoundError(detail)
        elif response.status_code == 429:
            raise ACISRateLimitError(detail, retry_after=float(response.headers.get("Retry-After", 0)))
        elif response.status_code == 503:
            raise ACISCircuitOpenError()
        else:
            raise ACISError(detail, code=body.get("code", "UNKNOWN"))

    def _post(self, path: str, json: dict) -> dict:
        return self._request("POST", path, json=json)

    def _get(self, path: str, params: dict = None) -> dict:
        return self._request("GET", path, params=params)

    def _delete(self, path: str) -> dict:
        return self._request("DELETE", path)

    # ─── Session Resolution ───────────────────────────────────────

    def _resolve_session(self, session_id=None, user_id=None, agent_id=None):
        if session_id:
            return session_id
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required for automatic session resolution")
        if self._resolved_session_id:
            return self._resolved_session_id
        result = self.create_session(user_id=uid, agent_id=agent_id)
        self._resolved_session_id = result.session_id
        return result.session_id

    # ─── Session Lifecycle ────────────────────────────────────────

    def create_session(self, user_id=None, agent_id=None, llm_model_id=None, token_budget=None, meta=None) -> SessionResult:
        """Create a new conversation session."""
        uid = user_id or self.user_id
        aid = agent_id or self.agent_id
        if not uid:
            raise ACISValidationError("user_id is required for create_session")
        self._resolved_session_id = None
        body = {"user_id": uid}
        if aid: body["agent_id"] = aid
        if llm_model_id: body["llm_model_id"] = llm_model_id
        if token_budget is not None: body["token_budget"] = token_budget
        if meta: body["meta"] = meta
        data = self._post("/sessions", json=body)
        return SessionResult(session_id=data["session_id"], tenant_id=data.get("tenant_id", self.tenant_id), user_id=uid, agent_id=data.get("agent_id", aid))

    def close_session(self, session_id=None, reason="user_ended") -> CloseResult:
        """Close a session (triggers async memory extraction)."""
        sid = session_id or self._resolved_session_id
        if not sid:
            raise ACISValidationError("session_id is required")
        data = self._post(f"/sessions/{sid}/close", json={"close_reason": reason})
        if sid == self._resolved_session_id:
            self._resolved_session_id = None
        return CloseResult(session_id=data.get("session_id", sid), closed=True, close_reason=data.get("close_reason", reason), closed_at=data.get("closed_at"), already_closed=data.get("already_closed", False))

    # ─── Write Events ─────────────────────────────────────────────

    def add_event(self, session_id=None, role="user", content="", event_type="message", user_id=None, agent_id=None, token_count=None, tool_call_id=None, idempotency_key=None, attachments=None) -> WriteResult:
        """Write a single event (message, tool_call, tool_result, etc.)."""
        if not content and event_type == "message":
            raise ACISValidationError("content is required for message events")
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required")
        resolved_sid = self._resolve_session(session_id, uid, agent_id or self.agent_id)
        body = {"session_id": resolved_sid, "user_id": uid, "event_type": event_type, "role": role, "content": content}
        if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
        if token_count is not None: body["token_count"] = token_count
        if tool_call_id: body["tool_call_id"] = tool_call_id
        if idempotency_key: body["idempotency_key"] = idempotency_key
        if attachments: body["attachments"] = attachments
        data = self._post("/events", json=body)
        return WriteResult(event_id=data["event_id"], seq=data["seq"], created=data["created"])

    def write_batch(self, session_id=None, events=None, user_id=None, agent_id=None) -> list:
        """Batch write multiple events atomically."""
        if not events or not isinstance(events, list):
            raise ACISValidationError("events must be a non-empty list")
        if len(events) > 50:
            raise ACISValidationError("write_batch limited to 50 events per call")
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required")
        resolved_sid = self._resolve_session(session_id, uid, agent_id or self.agent_id)
        body = {
            "session_id": resolved_sid, "user_id": uid,
            "events": [{"user_id": uid, "event_type": e.get("event_type", "message"), "role": e.get("role", "user"), "content": e.get("content", ""), "token_count": e.get("token_count"), "tool_call_id": e.get("tool_call_id"), "idempotency_key": e.get("idempotency_key")} for e in events],
        }
        if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
        data = self._post("/events/batch", json=body)
        return [WriteResult(event_id=e["event_id"], seq=e["seq"], created=e["created"]) for e in data.get("events", [])]

    # ─── Context Assembly ─────────────────────────────────────────

    def get_context(self, session_id=None, query=None, user_id=None, agent_id=None, model_id="gpt-4o") -> ContextResult:
        """Get token-budgeted context for your LLM call. Returns ctx.messages ready for any LLM.

        If query is None or empty, operates in "full recall" mode:
          - Returns all known memories (PINNED + HOT profile) without semantic filtering
          - Still includes STM events, summary, rules, identity, entity graph
          - Useful for workflow apps that need to know everything ACIS has about the user
        """
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required")
        resolved_sid = self._resolve_session(session_id, uid, agent_id or self.agent_id)
        body = {"session_id": resolved_sid, "user_id": uid}
        if query:
            body["query"] = query
        if model_id: body["model_id"] = model_id
        if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
        data = self._post("/context", json=body)
        raw_payload = data.get("context_payload", [])
        messages = self._build_messages(raw_payload, query or "")
        return ContextResult(messages=messages, context_payload=raw_payload, events_injected=data.get("events_injected", 0), memories_injected=data.get("memories_injected", 0), summary_injected=data.get("summary_injected", False), rules_injected=data.get("rules_injected", 0), total_tokens=data.get("total_tokens", 0), token_budget=data.get("token_budget", 0), remaining_budget=data.get("remaining_budget", 0), memory_ids_used=data.get("memory_ids_used", []))

    # ─── Knowledge Search ─────────────────────────────────────────

    def search(self, query: str, user_id=None, agent_id=None, top_k=10) -> list:
        """Search user memories (hybrid ANN + BM25)."""
        if not query:
            raise ACISValidationError("query is required for search")
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required for search")
        body = {"user_id": uid, "query": query, "top_k": top_k}
        if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
        data = self._post("/search", json=body)
        return [SearchResult(memory_id=r["memory_id"], content=r["content"], category=r["category"], importance=r["importance"], agent_id=r.get("agent_id"), entities=r.get("entities"), rrf_score=r.get("rrf_score")) for r in data.get("results", [])]

    # ─── Memory History ───────────────────────────────────────────

    def memory_history(self, memory_id: str) -> list:
        """Get temporal evolution chain for a memory."""
        if not memory_id:
            raise ACISValidationError("memory_id is required")
        data = self._get(f"/memories/{memory_id}/history")
        return [MemoryHistoryEntry(memory_id=i["memory_id"], content=i["content"], category=i["category"], importance=i["importance"], created_at=i.get("created_at"), valid_to=i.get("valid_to"), superseded_by=i.get("superseded_by")) for i in data.get("chain", [])]

    # ─── Feedback ─────────────────────────────────────────────────

    def feedback(self, memory_ids: list, user_id=None, useful=True) -> dict:
        """Submit retrieval usefulness feedback (best-effort, never raises)."""
        if not memory_ids:
            raise ACISValidationError("memory_ids is required")
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required for feedback")
        try:
            return self._post("/feedback", json={"memory_ids": memory_ids, "user_id": uid, "useful": useful})
        except ACISError:
            return {"status": "error", "count": 0}

    # ─── GDPR ─────────────────────────────────────────────────────

    def delete_user(self, user_id=None) -> dict:
        """Hard-delete all user data (GDPR right-to-erasure)."""
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required")
        return self._delete(f"/users/{uid}")

    def export_user(self, user_id=None) -> dict:
        """Export all user data (GDPR data portability)."""
        uid = user_id or self.user_id
        if not uid:
            raise ACISValidationError("user_id is required")
        return self._get(f"/users/{uid}/export")

    # ─── Message Builder ──────────────────────────────────────────

    def _build_messages(self, context_payload: list, query: str) -> list:
        messages = []
        system_parts = []
        for block in context_payload:
            t = block.get("type")
            if t == "rules":
                system_parts.append(f"[RULES]\n{block['content']}")
            elif t == "profile_summary":
                system_parts.append(block["content"])
            elif t == "entity_graph":
                system_parts.append(block["content"])
            elif t == "memories":
                inst = block.get("system_instruction", "")
                if inst: system_parts.append(inst)
                system_parts.append(block["content"])
            elif t == "summary":
                system_parts.append(f"[CONVERSATION SUMMARY]\n{block['content']}")
            elif t == "events":
                evts = block.get("content", [])
                if isinstance(evts, list):
                    for e in evts:
                        if e.get("content"):
                            messages.append({"role": e.get("role", "user"), "content": e["content"]})
        if system_parts:
            messages.insert(0, {"role": "system", "content": "\n\n".join(system_parts)})
        # Only append user message if query is non-empty (full-recall mode skips this)
        if query and (not messages or messages[-1].get("content") != query):
            messages.append({"role": "user", "content": query})
        return messages
