"""HTTP client for the AppXen Gateway API.

Sync httpx wrapper with Bearer token auth, 120s timeout, and retry on
transient errors (5xx, connection errors) with exponential backoff.
"""

from __future__ import annotations

import time
from pathlib import Path
from typing import Any, BinaryIO

import httpx

from appxen_cli import __version__

_MAX_RETRIES = 3
_RETRY_BACKOFF = [1.0, 2.0, 4.0]  # seconds between retries


class AppXenClient:
    """Sync HTTP client for the AppXen Gateway REST API."""

    def __init__(self, api_key: str, endpoint: str) -> None:
        self._client = httpx.Client(
            base_url=endpoint.rstrip("/"),
            headers={
                "Authorization": f"Bearer {api_key}",
                "User-Agent": f"appxen-cli/{__version__}",
            },
            timeout=120.0,
        )

    @property
    def endpoint(self) -> str:
        return str(self._client.base_url)

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

    def __enter__(self) -> "AppXenClient":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    # -- Helper --

    def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        last_exc: Exception | None = None
        for attempt in range(_MAX_RETRIES):
            try:
                resp = self._client.request(method, path, **kwargs)
                if resp.status_code < 500 or attempt == _MAX_RETRIES - 1:
                    resp.raise_for_status()
                    return resp
                # 5xx — retry
                last_exc = httpx.HTTPStatusError(
                    message=f"Server error {resp.status_code}",
                    request=resp.request,
                    response=resp,
                )
            except httpx.ConnectError as e:
                last_exc = e
            except httpx.HTTPStatusError:
                raise  # 4xx errors are not retried
            if attempt < _MAX_RETRIES - 1:
                time.sleep(_RETRY_BACKOFF[attempt])
        raise last_exc  # type: ignore[misc]

    # -- Health --

    def health(self) -> dict:
        return self._request("GET", "/health").json()

    # -- RAG Sources --

    def list_sources(
        self,
        *,
        limit: int = 20,
        offset: int = 0,
        status_filter: str | None = None,
        search: str | None = None,
    ) -> dict:
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if status_filter:
            params["status_filter"] = status_filter
        if search:
            params["search"] = search
        return self._request("GET", "/api/rag/sources", params=params).json()

    def get_source(self, source_id: str) -> dict:
        return self._request("GET", f"/api/rag/sources/{source_id}").json()

    def ingest_text(
        self,
        filename: str,
        content: str,
        metadata: dict | None = None,
    ) -> dict:
        payload: dict[str, Any] = {"filename": filename, "content": content}
        if metadata:
            payload["metadata"] = metadata
        return self._request("POST", "/api/rag/sources/ingest", json=payload).json()

    def upload_file(self, filepath: Path | str, file_obj: BinaryIO | None = None) -> dict:
        path = Path(filepath)
        if file_obj is None:
            with open(path, "rb") as f:
                return self._request(
                    "POST",
                    "/api/rag/sources/upload",
                    files={"file": (path.name, f, "application/octet-stream")},
                ).json()
        else:
            return self._request(
                "POST",
                "/api/rag/sources/upload",
                files={"file": (path.name, file_obj, "application/octet-stream")},
            ).json()

    def delete_source(self, source_id: str) -> None:
        self._request("DELETE", f"/api/rag/sources/{source_id}")

    # -- RAG Search --

    def search(self, query: str, top_k: int = 5) -> dict:
        return self._request(
            "POST",
            "/api/rag/search",
            json={"query": query, "top_k": top_k},
        ).json()

    def get_chunk(self, chunk_id: str) -> dict:
        return self._request("GET", f"/api/rag/chunks/{chunk_id}").json()

    # -- RAG Stats --

    def stats(self) -> dict:
        return self._request("GET", "/api/rag/stats").json()

    # -- Orchestrator --

    def orchestrator_health(self) -> dict:
        return self._request("GET", "/orchestrator/health").json()

    def compile_workflow(self, markdown: str) -> dict:
        return self._request(
            "POST", "/orchestrator/workflows/compile", json={"markdown": markdown}
        ).json()

    def create_workflow(self, name: str, markdown: str) -> dict:
        return self._request(
            "POST", "/orchestrator/workflows", json={"name": name, "markdown": markdown}
        ).json()

    def list_workflows(self) -> dict:
        return self._request("GET", "/orchestrator/workflows").json()

    def get_workflow(self, workflow_id: str) -> dict:
        return self._request("GET", f"/orchestrator/workflows/{workflow_id}").json()

    def update_workflow(self, workflow_id: str, markdown: str) -> dict:
        return self._request(
            "PUT", f"/orchestrator/workflows/{workflow_id}", json={"markdown": markdown}
        ).json()

    def delete_workflow(self, workflow_id: str) -> None:
        self._request("DELETE", f"/orchestrator/workflows/{workflow_id}")

    def run_workflow(self, workflow_id: str, inputs: dict | None = None) -> dict:
        return self._request(
            "POST",
            f"/orchestrator/workflows/{workflow_id}/run",
            json={"inputs": inputs or {}},
        ).json()

    def list_executions(self) -> dict:
        return self._request("GET", "/orchestrator/executions").json()

    def get_execution(self, execution_id: str) -> dict:
        return self._request("GET", f"/orchestrator/executions/{execution_id}").json()

    def get_steps(self, execution_id: str) -> dict:
        return self._request("GET", f"/orchestrator/executions/{execution_id}/steps").json()

    def stop_execution(self, execution_id: str) -> dict:
        return self._request("DELETE", f"/orchestrator/executions/{execution_id}").json()

    def approve_step(
        self, execution_id: str, step_id: str, approved: bool, reason: str = ""
    ) -> dict:
        body: dict[str, Any] = {"approved": approved}
        if reason:
            body["reason"] = reason
        return self._request(
            "POST",
            f"/orchestrator/executions/{execution_id}/steps/{step_id}/approve",
            json=body,
        ).json()
