"""HTTP client wrapper for the Docstream management API (spec §5.3, §6.6).

This module wraps the generated ``docstream_api_client`` SDK (spec §4.5 step 5,
§5.8) behind the same :class:`DocstreamClient` interface so the Typer commands
in ``main.py`` do not need to change.  Raw ``httpx`` calls have been replaced
by the SDK's ``sync_detailed`` functions, which return a :class:`Response`
object carrying the parsed model (or a parsed error model) and the HTTP status
code used for error reporting.
"""
from __future__ import annotations

import os
from typing import Any

import httpx
import typer
from rich.console import Console

from docstream_api_client import Client as SdkClient
from docstream_api_client.api.contexts import (
    contexts_service_add_source,
    contexts_service_create_context,
    contexts_service_delete_context,
    contexts_service_delete_source,
    contexts_service_list_contexts,
    contexts_service_reload_context,
    contexts_service_update_context,
)
from docstream_api_client.api.health import health_service_health
from docstream_api_client.models import (
    ConflictError,
    ContextCreate,
    ContextUpdate,
    NotFoundError,
    RendererConfig,
    RendererConfigHlEngine,
    RendererConfigMdEngine,
    SourceCreate,
    Transport,
    ValidationError,
)
from docstream_api_client.types import UNSET, Response, Unset

console = Console(stderr=True)

DEFAULT_BASE_URL = "http://localhost:49202/api/v1"
_API_PREFIX = "/api/v1"

# Map the CLI's free-form engine strings to the SDK's enum members.
_MD_ENGINES: dict[str, RendererConfigMdEngine] = {
    "pandoc": RendererConfigMdEngine.PANDOC,
    "python-markdown": RendererConfigMdEngine.PYTHON_MARKDOWN,
}
_HL_ENGINES: dict[str, RendererConfigHlEngine] = {
    "pygments": RendererConfigHlEngine.PYGMENTS,
    "none": RendererConfigHlEngine.NONE,
}


def _resolve_base_url(api_url: str | None) -> str:
    """Resolve the API base URL from the flag or ``DOCSTREAM_API_URL`` env var."""
    if api_url:
        return api_url.rstrip("/")
    env = os.environ.get("DOCSTREAM_API_URL")
    if env:
        return env.rstrip("/")
    return DEFAULT_BASE_URL


def _sdk_base_url(base_url: str) -> str:
    """Strip the ``/api/v1`` prefix so the SDK can re-add it itself.

    The generated SDK bakes ``/api/v1`` into every operation path (e.g.
    ``/api/v1/contexts/``), so the base URL handed to ``SdkClient`` must be the
    scheme + host only — otherwise the prefix would be duplicated.
    """
    url = base_url.rstrip("/")
    if url.endswith(_API_PREFIX):
        url = url[: -len(_API_PREFIX)]
    return url.rstrip("/")


def _renderer_dict(
    md_engine: str | None, hl_engine: str | None
) -> RendererConfig | Unset:
    """Build a :class:`RendererConfig` from optional engine strings.

    Returns ``UNSET`` when neither engine is provided so the SDK omits the
    ``renderer`` field from the request body entirely.  The name is kept for
    backwards-compatibility with ``main.py`` which imports this helper.
    """
    if not md_engine and not hl_engine:
        return UNSET
    md = _MD_ENGINES.get(md_engine, UNSET) if md_engine else UNSET
    hl = _HL_ENGINES.get(hl_engine, UNSET) if hl_engine else UNSET
    return RendererConfig(md_engine=md, hl_engine=hl)


class DocstreamClient:
    """Thin wrapper around the generated ``docstream_api_client`` SDK."""

    def __init__(self, base_url: str | None = None, *, timeout: float = 10.0) -> None:
        self.base_url = base_url or DEFAULT_BASE_URL
        self._timeout = timeout
        self._sdk = SdkClient(
            base_url=_sdk_base_url(self.base_url),
            timeout=httpx.Timeout(self._timeout),
        )

    # ------------------------------------------------------------------ helpers
    def _call(self, fn, /, **kwargs: Any) -> Response:
        """Invoke a SDK ``sync_detailed`` function with error handling."""
        try:
            resp = fn(**kwargs)
        except httpx.RequestError as exc:
            console.print(
                f"[red]Error:[/red] could not reach API at {self.base_url}: {exc}"
            )
            raise typer.Exit(1)
        if int(resp.status_code) >= 400:
            self._report_error(resp)
            raise typer.Exit(1)
        return resp

    def _report_error(self, resp: Response) -> None:
        parsed = resp.parsed
        status = int(resp.status_code)
        if isinstance(parsed, ConflictError):
            console.print(
                f"[red]Error[/red] ({status} {parsed.error.value}): "
                f"{parsed.resource} '{parsed.id}'"
            )
        elif isinstance(parsed, NotFoundError):
            console.print(
                f"[red]Error[/red] ({status} {parsed.error.value}): "
                f"{parsed.resource} '{parsed.id}'"
            )
        elif isinstance(parsed, ValidationError):
            field = f" [{parsed.field}]" if parsed.field is not UNSET else ""
            console.print(
                f"[red]Error[/red] ({status} {parsed.error.value}): "
                f"{parsed.message}{field}"
            )
        else:
            body = resp.content.decode(errors="ignore") if resp.content else ""
            console.print(f"[red]Error[/red] ({status}): {body}")

    # ------------------------------------------------------------------ contexts
    def create_context(
        self,
        context_id: str,
        renderer: RendererConfig | Unset | None = None,
    ) -> dict[str, Any]:
        body = ContextCreate(
            id=context_id,
            renderer=renderer if renderer is not None else UNSET,
        )
        resp = self._call(contexts_service_create_context.sync_detailed, client=self._sdk, body=body)
        return resp.parsed.to_dict() if resp.parsed is not None else {}

    def update_context(
        self,
        context_id: str,
        renderer: RendererConfig | Unset | None = None,
    ) -> dict[str, Any]:
        body = ContextUpdate(renderer=renderer if renderer is not None else UNSET)
        resp = self._call(
            contexts_service_update_context.sync_detailed,
            id=context_id,
            client=self._sdk,
            body=body,
        )
        return resp.parsed.to_dict() if resp.parsed is not None else {}

    def add_source(
        self,
        context_id: str,
        transport: str,
        location: str,
        alias: str | None = None,
        renderer: RendererConfig | Unset | None = None,
    ) -> dict[str, Any]:
        body = SourceCreate(
            transport=Transport(transport),
            location=location,
            alias=alias if alias else UNSET,
            renderer=renderer if renderer is not None else UNSET,
        )
        resp = self._call(
            contexts_service_add_source.sync_detailed,
            id=context_id,
            client=self._sdk,
            body=body,
        )
        return resp.parsed.to_dict() if resp.parsed is not None else {}

    def remove_source(self, context_id: str, slug: str) -> None:
        self._call(
            contexts_service_delete_source.sync_detailed,
            id=context_id,
            slug=slug,
            client=self._sdk,
        )

    def list_contexts(self) -> dict[str, Any]:
        resp = self._call(
            contexts_service_list_contexts.sync_detailed, client=self._sdk
        )
        return resp.parsed.to_dict() if resp.parsed is not None else {"contexts": []}

    def delete_context(self, context_id: str) -> None:
        self._call(
            contexts_service_delete_context.sync_detailed,
            id=context_id,
            client=self._sdk,
        )

    def reload_context(self, context_id: str) -> dict[str, Any]:
        resp = self._call(
            contexts_service_reload_context.sync_detailed,
            id=context_id,
            client=self._sdk,
        )
        if resp.parsed is None or int(resp.status_code) == 204 or not resp.content:
            return {"status": "reloaded"}
        return resp.parsed.to_dict() if hasattr(resp.parsed, "to_dict") else {}

    def health(self) -> dict[str, Any]:
        resp = self._call(health_service_health.sync_detailed, client=self._sdk)
        return resp.parsed.to_dict() if resp.parsed is not None else {}
