"""HTTP client and top-level SDK interface."""

from __future__ import annotations

import os
from contextlib import AbstractContextManager, contextmanager
from typing import Any, Dict, Iterable, Iterator, Optional

import httpx


DEFAULT_BASE_URL = os.getenv("EM_API_BASE_URL", "http://localhost:8100")


class APIError(RuntimeError):
    """Represents an HTTP error raised by the Emotion Machine API."""

    def __init__(self, status_code: int, message: str, payload: Optional[dict] = None) -> None:
        super().__init__(f"HTTP {status_code}: {message}")
        self.status_code = status_code
        self.message = message
        self.payload = payload or {}


class _HTTPClient(AbstractContextManager["_HTTPClient"]):
    """Minimal HTTPX wrapper with shared error handling."""

    def __init__(
        self,
        api_key: str,
        base_url: str,
        *,
        timeout: Optional[float] = 30.0,
    ) -> None:
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": "emotion-machine-sdk/0",
        }
        self._client = httpx.Client(base_url=base_url, timeout=timeout, headers=headers)

    def close(self) -> None:
        self._client.close()

    def __exit__(self, *exc_info: object) -> None:  # pragma: no cover - delegated
        self.close()

    def request(self, method: str, path: str, *, expected: Iterable[int] = (200, 201), **kwargs: Any) -> Any:
        response = self._client.request(method, path, **kwargs)
        if response.status_code not in expected:
            detail: Dict[str, Any]
            try:
                detail = response.json()
            except Exception:  # pragma: no cover - defensive
                detail = {"detail": response.text}
            message = detail.get("detail") or detail.get("message") or response.text
            raise APIError(response.status_code, message, payload=detail)
        if response.headers.get("Content-Type", "").startswith("application/json"):
            return response.json()
        return response.text

    @contextmanager
    def stream(self, method: str, path: str, *, expected: Iterable[int] = (200,), **kwargs: Any) -> Iterator[httpx.Response]:
        with self._client.stream(method, path, **kwargs) as response:
            if response.status_code not in expected:
                body = response.read()
                try:
                    detail = httpx.Response(200, content=body).json()
                except Exception:  # pragma: no cover - defensive
                    detail = {"detail": body.decode()}
                message = detail.get("detail") or detail.get("message") or body.decode()
                raise APIError(response.status_code, message, payload=detail)
            yield response


class EmotionMachine:
    """Primary SDK entrypoint."""

    def __init__(
        self,
        *,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: Optional[float] = 30.0,
    ) -> None:
        key = api_key or os.getenv("EM_API_KEY")
        if not key:
            raise ValueError("API key is required. Set EM_API_KEY or pass api_key explicitly.")
        base = (base_url or DEFAULT_BASE_URL).rstrip("/")
        self._http = _HTTPClient(key, base, timeout=timeout)

        # Lazily import to avoid circular references during packaging.
        from .resources.chat import ChatAPI
        from .resources.companions import CompanionAPI
        from .resources.conversations import ConversationAPI
        from .resources.knowledge import KnowledgeAPI

        self.companions = CompanionAPI(self._http)
        self.knowledge = KnowledgeAPI(self._http)
        self.chat = ChatAPI(self._http)
        self.conversations = ConversationAPI(self._http)

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> "EmotionMachine":  # pragma: no cover - context convenience
        return self

    def __exit__(self, *exc_info: object) -> None:  # pragma: no cover - context convenience
        self.close()

    # Convenience methods delegating to resource helpers
    def list_companions(self) -> list[dict[str, Any]]:
        return self.companions.list()

    def create_companion(self, *, name: str, description: Optional[str] = None, config: Optional[Dict[str, Any]] = None) -> dict[str, Any]:
        return self.companions.create(name=name, description=description, config=config)

    def get_companion(self, companion_id: str) -> dict[str, Any]:
        return self.companions.get(companion_id)

    def update_companion(
        self,
        companion_id: str,
        *,
        name: Optional[str] = None,
        description: Optional[str] = None,
        config: Optional[Dict[str, Any]] = None,
    ) -> dict[str, Any]:
        return self.companions.update(companion_id, name=name, description=description, config=config)

    def upsert_profile_schema(self, companion_id: str, schema: Dict[str, Any]) -> dict[str, Any]:
        return self.companions.profile_schema_upsert(companion_id, schema)

    def get_profile_schema(self, companion_id: str) -> dict[str, Any]:
        return self.companions.profile_schema_get(companion_id)

    def ingest_knowledge(
        self,
        companion_id: str,
        *,
        payload_type: str = "text",
        content: Optional[str] = None,
        key: Optional[str] = None,
    ) -> dict[str, Any]:
        return self.knowledge.ingest(companion_id, payload_type=payload_type, content=content, key=key)

    def wait_for_job(self, job_id: str, *, timeout: float = 30.0, interval: float = 0.5) -> dict[str, Any]:
        return self.knowledge.wait(job_id, timeout=timeout, interval=interval)

    def chat_completion(
        self,
        companion_id: str,
        *,
        message: str,
        external_user_id: str,
        model: Optional[str] = None,
        temperature: Optional[float] = None,
        conversation_id: Optional[str] = None,
        profile: Optional[Dict[str, Any]] = None,
    ) -> dict[str, Any]:
        return self.chat.complete(
            companion_id,
            message=message,
            external_user_id=external_user_id,
            model=model,
            temperature=temperature,
            conversation_id=conversation_id,
            profile=profile,
        )

    def chat_stream(
        self,
        companion_id: str,
        *,
        message: str,
        external_user_id: str,
        model: Optional[str] = None,
        temperature: Optional[float] = None,
        conversation_id: Optional[str] = None,
        profile: Optional[Dict[str, Any]] = None,
    ) -> Iterator[dict[str, Any]]:
        return self.chat.stream(
            companion_id,
            message=message,
            external_user_id=external_user_id,
            model=model,
            temperature=temperature,
            conversation_id=conversation_id,
            profile=profile,
        )

    def get_conversation(self, conversation_id: str) -> dict[str, Any]:
        return self.conversations.get(conversation_id)
