from __future__ import annotations

import json
from collections.abc import AsyncIterator
from typing import Any

import httpx

from .downstream import DownstreamProtocolError, make_timeout
from .models import HermesRun, HermesRunRequest
from .sse import SSEEvent, parse_sse_lines


class HermesAdapter:
    """Client contract for Hermes Agent: create run, consume SSE, stop run."""

    def __init__(
        self,
        base_url: str,
        *,
        api_key: str | None = None,
        connect_timeout_s: float = 0.75,
        request_timeout_s: float = 20.0,
        stream_timeout_s: float = 300.0,
        client: httpx.AsyncClient | None = None,
    ) -> None:
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self._owns_client = client is None
        self.client = client or httpx.AsyncClient(
            timeout=make_timeout(connect_timeout_s, request_timeout_s, stream_timeout_s)
        )

    async def close(self) -> None:
        if self._owns_client:
            await self.client.aclose()

    async def create_run(self, request: HermesRunRequest, *, idempotency_key: str) -> HermesRun:
        body: dict[str, Any] = {
            "input": request.prompt,
            "session_id": request.session_id,
        }
        delegated_context = {
            "target_node": request.target_node,
            "required_capabilities": sorted(request.required_capabilities),
            "requested_permissions": sorted(request.requested_permissions),
            "needs_vision": request.needs_vision,
            **request.context,
        }
        delegated_context = {
            key: value
            for key, value in delegated_context.items()
            if value not in (None, False, [], {}, "")
        }
        if delegated_context:
            encoded = json.dumps(
                delegated_context,
                ensure_ascii=True,
                sort_keys=True,
                separators=(",", ":"),
            )
            body["instructions"] = f"OMNI_CONTEXT_V1 {encoded}"[:2000]
        response = await self.client.post(
            f"{self.base_url}/v1/runs",
            headers=self._headers(
                accept="application/json", idempotency_key=idempotency_key
            ),
            json=body,
        )
        response.raise_for_status()
        payload = response.json()
        run_id = payload.get("run_id") or payload.get("id")
        if not isinstance(run_id, str) or not run_id:
            raise DownstreamProtocolError("Hermes create-run response has no run_id")
        return HermesRun(run_id=run_id)

    async def events(self, run_id: str, *, after_event_id: str | None = None) -> AsyncIterator[SSEEvent]:
        headers = self._headers(accept="text/event-stream")
        if after_event_id:
            headers["Last-Event-ID"] = after_event_id
        async with self.client.stream(
            "GET", f"{self.base_url}/v1/runs/{run_id}/events", headers=headers
        ) as response:
            response.raise_for_status()
            async for event in parse_sse_lines(response.aiter_lines()):
                yield event

    async def stop(self, run_id: str) -> None:
        response = await self.client.post(
            f"{self.base_url}/v1/runs/{run_id}/stop", headers=self._headers()
        )
        if response.status_code not in {200, 202, 204, 404, 409}:
            response.raise_for_status()

    async def approve(self, run_id: str, choice: str) -> None:
        if choice not in {"once", "deny"}:
            raise ValueError("Voice approval is limited to once or deny")
        response = await self.client.post(
            f"{self.base_url}/v1/runs/{run_id}/approval",
            headers=self._headers(accept="application/json"),
            json={"choice": choice},
        )
        response.raise_for_status()

    async def health(self) -> dict[str, Any]:
        try:
            response = await self.client.get(
                f"{self.base_url}/health", headers=self._headers()
            )
            response.raise_for_status()
            return {"status": "ready", "detail": response.json()}
        except (httpx.HTTPError, ValueError) as exc:
            return {"status": "unavailable", "detail": type(exc).__name__}

    def _headers(
        self, *, accept: str | None = None, idempotency_key: str | None = None
    ) -> dict[str, str]:
        headers: dict[str, str] = {}
        if accept:
            headers["Accept"] = accept
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
        return headers
