"""Sync and async HTTP clients for the Monopigi API."""

from __future__ import annotations

import asyncio
import contextlib
import json
import time
from collections.abc import AsyncIterator, Iterator
from pathlib import Path
from typing import TYPE_CHECKING

import httpx

from monopigi.cache import DiskCache
from monopigi.config import DEFAULT_BASE_URL, Config, load_config
from monopigi.exceptions import AuthError, MonopigiError, NotFoundError, RateLimitError, TierError
from monopigi.models import (
    Document,
    DocumentsResponse,
    QuotaInfo,
    SearchResponse,
    Source,
    StatsResponse,
    UsageResponse,
)

if TYPE_CHECKING:
    from monopigi.sources import (
        AsyncDataGovGrSource,
        AsyncDiavgeiaSource,
        AsyncElstatSource,
        AsyncGeodataSource,
        AsyncKimdisSource,
        AsyncMitosSource,
        AsyncRaeSource,
        AsyncTedSource,
        DataGovGrSource,
        DiavgeiaSource,
        ElstatSource,
        GeodataSource,
        KimdisSource,
        MitosSource,
        RaeSource,
        TedSource,
    )


def _resolve_config(token: str, base_url: str) -> tuple[str, str]:
    """Resolve token and base_url from args or stored config."""
    if not token or not base_url:
        cfg: Config = load_config()
        token = token or cfg.token
        base_url = base_url or cfg.base_url
    if not token:
        raise AuthError("No API token provided. Run `monopigi auth login <token>` or pass token= to Client.")
    return token, base_url or DEFAULT_BASE_URL


def _handle_error(resp: httpx.Response, *, current_tier: str | None = None) -> None:
    """Raise typed exceptions for HTTP error responses."""
    if resp.status_code == 401:
        raise AuthError("Invalid or missing API token")
    if resp.status_code == 403:
        detail = ""
        with contextlib.suppress(ValueError, KeyError):
            detail = resp.json().get("detail", "") if resp.content else ""
        required = _parse_required_tier(detail)
        if required:
            raise TierError(
                required_tier=required,
                current_tier=current_tier or "unknown",
                endpoint=str(resp.url.path) if hasattr(resp.url, "path") else str(resp.url),
            )
        raise MonopigiError(f"API error 403: {detail or resp.text}")
    if resp.status_code == 429:
        reset = resp.headers.get("X-RateLimit-Reset", "")
        raise RateLimitError("Daily query quota exceeded", reset_at=reset)
    if resp.status_code == 404:
        try:
            detail = resp.json().get("detail", "Not found") if resp.content else "Not found"
        except ValueError, KeyError:
            detail = "Not found"
        raise NotFoundError(detail)
    if resp.status_code >= 400:
        raise MonopigiError(f"API error {resp.status_code}: {resp.text}")


def _parse_required_tier(detail: str) -> str | None:
    """Extract tier name from a 403 detail like 'This feature requires a Pro subscription.'."""
    if not detail:
        return None
    detail_lower = detail.lower()
    for tier in ("enterprise", "pro", "free"):
        if tier in detail_lower:
            return tier
    return None


def _parse_quota_headers(headers: httpx.Headers) -> QuotaInfo | None:
    """Parse rate-limit headers into QuotaInfo, or None if absent."""
    limit = headers.get("X-RateLimit-Limit")
    remaining = headers.get("X-RateLimit-Remaining")
    reset = headers.get("X-RateLimit-Reset")
    if limit is not None and remaining is not None and reset is not None:
        try:
            return QuotaInfo(limit=int(limit), remaining=int(remaining), reset=reset)
        except ValueError, TypeError:
            return None
    return None


def _parse_tier_header(headers: httpx.Headers) -> str | None:
    """Parse the X-Tier header, if present."""
    return headers.get("X-Tier")


TIER_FEATURES: dict[str, set[str]] = {
    "free": {"metadata", "basic_search"},
    "pro": {"metadata", "basic_search", "full_text", "content"},
    "enterprise": {"metadata", "basic_search", "full_text", "content", "rag", "mcp", "entities", "similar"},
}


def _build_doc_params(limit: int, offset: int, since: str | None) -> dict[str, str | int]:
    """Build query params for document endpoints."""
    params: dict[str, str | int] = {"limit": limit, "offset": offset}
    if since:
        params["since"] = since
    return params


class MonopigiClient:
    """Synchronous client for the Monopigi API.

    Usage:
        with MonopigiClient("mp_live_...") as client:
            results = client.search("hospital procurement")
    """

    def __init__(
        self,
        token: str = "",
        base_url: str = "",
        max_retries: int = 3,
        cache_ttl: int | None = None,
    ) -> None:
        token, base_url = _resolve_config(token, base_url)
        self._max_retries = max_retries
        self._cache: DiskCache | None = DiskCache(cache_ttl) if cache_ttl else None
        self._tier: str | None = None
        self._quota: QuotaInfo | None = None
        self._client = httpx.Client(
            base_url=base_url,
            headers={"Authorization": f"Bearer {token}"},
            timeout=30.0,
        )

    @property
    def tier(self) -> str | None:
        """Current API key's tier (free/pro/enterprise), populated after first request."""
        return self._tier

    @property
    def quota(self) -> QuotaInfo | None:
        """Current quota info, populated after first request."""
        return self._quota

    def has_feature(self, feature: str) -> bool:
        """Check if the current tier includes a feature."""
        if not self._tier:
            return False
        return feature in TIER_FEATURES.get(self._tier, set())

    def _update_tier_info(self, resp: httpx.Response) -> None:
        """Update tier and quota from response headers."""
        tier = _parse_tier_header(resp.headers)
        if tier:
            self._tier = tier
        quota = _parse_quota_headers(resp.headers)
        if quota:
            self._quota = quota

    def _request_with_retry(self, method: str, path: str, **kwargs: object) -> httpx.Response:
        """Make a request with automatic retry on 429 rate-limit responses."""
        resp: httpx.Response | None = None
        for attempt in range(self._max_retries + 1):
            resp = self._client.request(method, path, **kwargs)  # type: ignore[arg-type]
            self._update_tier_info(resp)
            if resp.status_code == 429 and attempt < self._max_retries:
                reset = resp.headers.get("X-RateLimit-Reset", "")
                wait = max(1, int(reset) - int(time.time())) if reset.isdigit() else 60
                wait = min(wait, 300)  # cap at 5 min
                time.sleep(wait)
                continue
            if resp.status_code >= 400:
                _handle_error(resp, current_tier=self._tier)
            return resp
        assert resp is not None
        _handle_error(resp, current_tier=self._tier)
        raise AssertionError("unreachable")  # pragma: no cover

    def _request(self, method: str, path: str, **kwargs: object) -> httpx.Response:
        if self._cache is not None:
            cache_params = json.dumps(kwargs.get("params", {}), sort_keys=True)
            url = str(self._client.base_url) + path
            cached = self._cache.get(method, url, cache_params)
            if cached is not None:
                return httpx.Response(200, text=cached)
            resp = self._request_with_retry(method, path, **kwargs)
            self._cache.set(method, url, cache_params, resp.text)
            return resp
        return self._request_with_retry(method, path, **kwargs)

    def sources(self) -> list[Source]:
        """List available data sources."""
        resp = self._request("GET", "/v1/sources")
        return [Source(**s) for s in resp.json()]

    def search(self, query: str, source: str | None = None, limit: int = 100, offset: int = 0) -> SearchResponse:
        """Search across all sources, or filtered to a specific source."""
        params: dict[str, str | int] = {"q": query, "limit": limit, "offset": offset}
        if source:
            params["source"] = source
        resp = self._request("GET", "/v1/search", params=params)
        return SearchResponse(**resp.json())

    def documents(self, source: str, limit: int = 100, offset: int = 0, since: str | None = None) -> DocumentsResponse:
        """Query documents from a specific source."""
        resp = self._request("GET", f"/v1/{source}/documents", params=_build_doc_params(limit, offset, since))
        return DocumentsResponse(**resp.json())

    def stats(self) -> StatsResponse:
        """Get platform-wide statistics."""
        resp = self._request("GET", "/v1/stats")
        return StatsResponse(**resp.json())

    def usage(self) -> UsageResponse:
        """Get your API usage for today."""
        resp = self._request("GET", "/v1/usage")
        return UsageResponse(**resp.json())

    def models(self) -> dict:
        """List available LLM models for the /v1/ask endpoint. No auth required."""
        resp = self._client.get("/v1/models")
        if resp.status_code >= 400:
            _handle_error(resp, current_tier=self._tier)
        return resp.json()

    # -- Enterprise features ---------------------------------------------------

    def ask(self, question: str, limit: int = 5, model: str | None = None) -> dict:
        """Ask a natural language question (RAG). Enterprise only."""
        payload: dict = {"question": question, "limit": limit}
        if model:
            payload["model"] = model
        resp = self._request("POST", "/v1/ask", json=payload)
        return resp.json()

    def entity(self, identifier: str, identifier_type: str = "afm") -> dict:
        """Resolve an entity by AFM, name, or ADA. Enterprise only."""
        resp = self._request("GET", f"/v1/entity/{identifier}", params={"type": identifier_type})
        return resp.json()

    def similar(self, source_id: str, limit: int = 10) -> dict:
        """Find similar documents. Enterprise only."""
        resp = self._request("GET", f"/v1/similar/{source_id}", params={"limit": limit})
        return resp.json()

    def content(self, source_id: str) -> bytes:
        """Download original document content (PDF/XML/JSON). Enterprise only."""
        resp = self._request("GET", f"/v1/documents/{source_id}/content")
        return resp.content

    # -- Reports (Pro+) --------------------------------------------------------

    def create_report(self, entity_identifier: str, identifier_type: str = "afm") -> dict:
        """Create a due diligence report request. Pro tier and above."""
        resp = self._request(
            "POST", "/v1/reports", json={"entity_identifier": entity_identifier, "identifier_type": identifier_type}
        )
        return resp.json()

    def get_report(self, report_id: str) -> dict:
        """Get a report by ID."""
        resp = self._request("GET", f"/v1/reports/{report_id}")
        return resp.json()

    def list_reports(self, limit: int = 20, offset: int = 0) -> dict:
        """List report requests."""
        resp = self._request("GET", "/v1/reports", params={"limit": limit, "offset": offset})
        return resp.json()

    def get_report_pdf(self, report_id: str) -> bytes:
        """Download PDF for a completed report."""
        resp = self._request("GET", f"/v1/reports/{report_id}/pdf")
        return resp.content

    # -- Alerts (Enterprise) ---------------------------------------------------

    def create_alert_profile(
        self, name: str, filters: dict, channels: list[str] | None = None, **kwargs: object
    ) -> dict:
        """Create an alert profile. Enterprise only."""
        payload: dict = {"name": name, "filters": filters}
        if channels:
            payload["channels"] = channels
        payload.update(kwargs)
        resp = self._request("POST", "/v1/alerts/profiles", json=payload)
        return resp.json()

    def list_alert_profiles(self, limit: int = 20, offset: int = 0) -> dict:
        """List alert profiles."""
        resp = self._request("GET", "/v1/alerts/profiles", params={"limit": limit, "offset": offset})
        return resp.json()

    def update_alert_profile(self, profile_id: str, **kwargs: object) -> dict:
        """Update an alert profile."""
        resp = self._request("PUT", f"/v1/alerts/profiles/{profile_id}", json=kwargs)
        return resp.json()

    def delete_alert_profile(self, profile_id: str) -> dict:
        """Delete an alert profile."""
        resp = self._request("DELETE", f"/v1/alerts/profiles/{profile_id}")
        return resp.json()

    def list_alert_deliveries(self, profile_id: str | None = None, limit: int = 20, offset: int = 0) -> dict:
        """List alert deliveries."""
        params: dict[str, str | int] = {"limit": limit, "offset": offset}
        if profile_id:
            params["profile_id"] = profile_id
        resp = self._request("GET", "/v1/alerts/deliveries", params=params)
        return resp.json()

    # -- Compliance monitoring (Enterprise) ------------------------------------

    def add_monitored_entity(
        self, entity_identifier: str, identifier_type: str = "afm", label: str | None = None
    ) -> dict:
        """Add an entity to monitor. Enterprise only."""
        payload: dict = {"entity_identifier": entity_identifier, "identifier_type": identifier_type}
        if label:
            payload["label"] = label
        resp = self._request("POST", "/v1/monitor/entities", json=payload)
        return resp.json()

    def list_monitored_entities(self, limit: int = 20, offset: int = 0) -> dict:
        """List monitored entities."""
        resp = self._request("GET", "/v1/monitor/entities", params={"limit": limit, "offset": offset})
        return resp.json()

    def remove_monitored_entity(self, entity_id: str) -> dict:
        """Remove (deactivate) a monitored entity."""
        resp = self._request("DELETE", f"/v1/monitor/entities/{entity_id}")
        return resp.json()

    def list_entity_events(
        self,
        entity_id: str | None = None,
        event_type: str | None = None,
        since: str | None = None,
        limit: int = 20,
        offset: int = 0,
    ) -> dict:
        """List events for monitored entities."""
        params: dict[str, str | int] = {"limit": limit, "offset": offset}
        if entity_id:
            params["entity_id"] = entity_id
        if event_type:
            params["event_type"] = event_type
        if since:
            params["since"] = since
        resp = self._request("GET", "/v1/monitor/events", params=params)
        return resp.json()

    def acknowledge_event(self, event_id: str) -> dict:
        """Acknowledge a monitoring event."""
        resp = self._request("POST", f"/v1/monitor/events/{event_id}/acknowledge")
        return resp.json()

    def entity_health_report(self, entity_id: str) -> dict:
        """Trigger a health report for a monitored entity."""
        resp = self._request("GET", f"/v1/monitor/entities/{entity_id}/report")
        return resp.json()

    # -- Pagination iterators --------------------------------------------------

    def search_iter(self, query: str, page_size: int = 100) -> Iterator[Document]:
        """Iterate through all search results, auto-paginating."""
        offset = 0
        while True:
            resp = self.search(query, limit=page_size, offset=offset)
            yield from resp.results
            offset += page_size
            if offset >= resp.total:
                break

    def documents_iter(self, source: str, page_size: int = 100, since: str | None = None) -> Iterator[Document]:
        """Iterate through all documents from a source, auto-paginating."""
        offset = 0
        while True:
            resp = self.documents(source, limit=page_size, offset=offset, since=since)
            yield from resp.documents
            offset += page_size
            if offset >= resp.total:
                break

    # -- Bulk export -----------------------------------------------------------

    def export(
        self,
        source: str,
        path: str,
        format: str = "json",
        since: str | None = None,
        limit: int | None = None,
    ) -> int:
        """Export documents from a source to a file. Returns number of documents exported.

        Formats: json, csv, parquet (requires polars)
        """
        from monopigi.progress import iter_with_progress

        # First get total count
        resp = self.documents(source, limit=1, since=since)
        total = resp.total

        # Then iterate with progress
        raw_iter = self.documents_iter(source, since=since)
        docs = list(iter_with_progress(raw_iter, total=total, description=f"Exporting {source}"))
        if limit:
            docs = docs[:limit]

        data = [doc.model_dump() for doc in docs]

        if format == "json":
            import json as _json

            Path(path).write_text(_json.dumps(data, indent=2, ensure_ascii=False))
        elif format == "csv":
            import csv

            if data:
                with open(path, "w", newline="") as f:
                    writer = csv.DictWriter(f, fieldnames=data[0].keys())
                    writer.writeheader()
                    writer.writerows({k: str(v) if v is not None else "" for k, v in row.items()} for row in data)
        elif format == "parquet":
            import polars as pl

            pl.DataFrame(data).write_parquet(path)
        else:
            raise ValueError(f"Unsupported format: {format}. Use json, csv, or parquet.")

        return len(docs)

    # -- Source-specific typed clients ------------------------------------------

    @property
    def ted(self) -> TedSource:
        from monopigi.sources import TedSource

        return TedSource(self)

    @property
    def diavgeia(self) -> DiavgeiaSource:
        from monopigi.sources import DiavgeiaSource

        return DiavgeiaSource(self)

    @property
    def elstat(self) -> ElstatSource:
        from monopigi.sources import ElstatSource

        return ElstatSource(self)

    @property
    def rae(self) -> RaeSource:
        from monopigi.sources import RaeSource

        return RaeSource(self)

    @property
    def data_gov_gr(self) -> DataGovGrSource:
        from monopigi.sources import DataGovGrSource

        return DataGovGrSource(self)

    @property
    def mitos(self) -> MitosSource:
        from monopigi.sources import MitosSource

        return MitosSource(self)

    @property
    def kimdis(self) -> KimdisSource:
        from monopigi.sources import KimdisSource

        return KimdisSource(self)

    @property
    def geodata(self) -> GeodataSource:
        from monopigi.sources import GeodataSource

        return GeodataSource(self)

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

    def __enter__(self) -> MonopigiClient:
        return self

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


class AsyncMonopigiClient:
    """Async client for the Monopigi API.

    Usage:
        async with AsyncMonopigiClient("mp_live_...") as client:
            results = await client.search("hospital procurement")
    """

    def __init__(
        self,
        token: str = "",
        base_url: str = "",
        max_retries: int = 3,
        cache_ttl: int | None = None,
    ) -> None:
        token, base_url = _resolve_config(token, base_url)
        self._max_retries = max_retries
        self._cache: DiskCache | None = DiskCache(cache_ttl) if cache_ttl else None
        self._tier: str | None = None
        self._quota: QuotaInfo | None = None
        self._client = httpx.AsyncClient(
            base_url=base_url,
            headers={"Authorization": f"Bearer {token}"},
            timeout=30.0,
        )

    @property
    def tier(self) -> str | None:
        """Current API key's tier (free/pro/enterprise), populated after first request."""
        return self._tier

    @property
    def quota(self) -> QuotaInfo | None:
        """Current quota info, populated after first request."""
        return self._quota

    def has_feature(self, feature: str) -> bool:
        """Check if the current tier includes a feature."""
        if not self._tier:
            return False
        return feature in TIER_FEATURES.get(self._tier, set())

    def _update_tier_info(self, resp: httpx.Response) -> None:
        """Update tier and quota from response headers."""
        tier = _parse_tier_header(resp.headers)
        if tier:
            self._tier = tier
        quota = _parse_quota_headers(resp.headers)
        if quota:
            self._quota = quota

    async def _request_with_retry(self, method: str, path: str, **kwargs: object) -> httpx.Response:
        """Make a request with automatic retry on 429 rate-limit responses."""
        resp: httpx.Response | None = None
        for attempt in range(self._max_retries + 1):
            resp = await self._client.request(method, path, **kwargs)  # type: ignore[arg-type]
            self._update_tier_info(resp)
            if resp.status_code == 429 and attempt < self._max_retries:
                reset = resp.headers.get("X-RateLimit-Reset", "")
                wait = max(1, int(reset) - int(time.time())) if reset.isdigit() else 60
                wait = min(wait, 300)  # cap at 5 min
                await asyncio.sleep(wait)
                continue
            if resp.status_code >= 400:
                _handle_error(resp, current_tier=self._tier)
            return resp
        assert resp is not None
        _handle_error(resp, current_tier=self._tier)
        raise AssertionError("unreachable")  # pragma: no cover

    async def _request(self, method: str, path: str, **kwargs: object) -> httpx.Response:
        if self._cache is not None:
            cache_params = json.dumps(kwargs.get("params", {}), sort_keys=True)
            url = str(self._client.base_url) + path
            cached = self._cache.get(method, url, cache_params)
            if cached is not None:
                return httpx.Response(200, text=cached)
            resp = await self._request_with_retry(method, path, **kwargs)
            self._cache.set(method, url, cache_params, resp.text)
            return resp
        return await self._request_with_retry(method, path, **kwargs)

    async def sources(self) -> list[Source]:
        """List available data sources."""
        resp = await self._request("GET", "/v1/sources")
        return [Source(**s) for s in resp.json()]

    async def search(self, query: str, source: str | None = None, limit: int = 100, offset: int = 0) -> SearchResponse:
        """Search across all sources, or filtered to a specific source."""
        params: dict[str, str | int] = {"q": query, "limit": limit, "offset": offset}
        if source:
            params["source"] = source
        resp = await self._request("GET", "/v1/search", params=params)
        return SearchResponse(**resp.json())

    async def documents(
        self, source: str, limit: int = 100, offset: int = 0, since: str | None = None
    ) -> DocumentsResponse:
        """Query documents from a specific source."""
        resp = await self._request("GET", f"/v1/{source}/documents", params=_build_doc_params(limit, offset, since))
        return DocumentsResponse(**resp.json())

    async def stats(self) -> StatsResponse:
        """Get platform-wide statistics."""
        resp = await self._request("GET", "/v1/stats")
        return StatsResponse(**resp.json())

    async def usage(self) -> UsageResponse:
        """Get your API usage for today."""
        resp = await self._request("GET", "/v1/usage")
        return UsageResponse(**resp.json())

    async def models(self) -> dict:
        """List available LLM models for the /v1/ask endpoint. No auth required."""
        resp = await self._client.get("/v1/models")
        if resp.status_code >= 400:
            _handle_error(resp, current_tier=self._tier)
        return resp.json()

    # -- Enterprise features ---------------------------------------------------

    async def ask(self, question: str, limit: int = 5, model: str | None = None) -> dict:
        """Ask a natural language question (RAG). Enterprise only."""
        payload: dict = {"question": question, "limit": limit}
        if model:
            payload["model"] = model
        resp = await self._request("POST", "/v1/ask", json=payload)
        return resp.json()

    async def entity(self, identifier: str, identifier_type: str = "afm") -> dict:
        """Resolve an entity by AFM, name, or ADA. Enterprise only."""
        resp = await self._request("GET", f"/v1/entity/{identifier}", params={"type": identifier_type})
        return resp.json()

    async def similar(self, source_id: str, limit: int = 10) -> dict:
        """Find similar documents. Enterprise only."""
        resp = await self._request("GET", f"/v1/similar/{source_id}", params={"limit": limit})
        return resp.json()

    async def content(self, source_id: str) -> bytes:
        """Download original document content (PDF/XML/JSON). Enterprise only."""
        resp = await self._request("GET", f"/v1/documents/{source_id}/content")
        return resp.content

    # -- Reports (Pro+) --------------------------------------------------------

    async def create_report(self, entity_identifier: str, identifier_type: str = "afm") -> dict:
        """Create a due diligence report request. Pro tier and above."""
        resp = await self._request(
            "POST", "/v1/reports", json={"entity_identifier": entity_identifier, "identifier_type": identifier_type}
        )
        return resp.json()

    async def get_report(self, report_id: str) -> dict:
        """Get a report by ID."""
        resp = await self._request("GET", f"/v1/reports/{report_id}")
        return resp.json()

    async def list_reports(self, limit: int = 20, offset: int = 0) -> dict:
        """List report requests."""
        resp = await self._request("GET", "/v1/reports", params={"limit": limit, "offset": offset})
        return resp.json()

    async def get_report_pdf(self, report_id: str) -> bytes:
        """Download PDF for a completed report."""
        resp = await self._request("GET", f"/v1/reports/{report_id}/pdf")
        return resp.content

    # -- Alerts (Enterprise) ---------------------------------------------------

    async def create_alert_profile(
        self, name: str, filters: dict, channels: list[str] | None = None, **kwargs: object
    ) -> dict:
        """Create an alert profile. Enterprise only."""
        payload: dict = {"name": name, "filters": filters}
        if channels:
            payload["channels"] = channels
        payload.update(kwargs)
        resp = await self._request("POST", "/v1/alerts/profiles", json=payload)
        return resp.json()

    async def list_alert_profiles(self, limit: int = 20, offset: int = 0) -> dict:
        """List alert profiles."""
        resp = await self._request("GET", "/v1/alerts/profiles", params={"limit": limit, "offset": offset})
        return resp.json()

    async def update_alert_profile(self, profile_id: str, **kwargs: object) -> dict:
        """Update an alert profile."""
        resp = await self._request("PUT", f"/v1/alerts/profiles/{profile_id}", json=kwargs)
        return resp.json()

    async def delete_alert_profile(self, profile_id: str) -> dict:
        """Delete an alert profile."""
        resp = await self._request("DELETE", f"/v1/alerts/profiles/{profile_id}")
        return resp.json()

    async def list_alert_deliveries(self, profile_id: str | None = None, limit: int = 20, offset: int = 0) -> dict:
        """List alert deliveries."""
        params: dict[str, str | int] = {"limit": limit, "offset": offset}
        if profile_id:
            params["profile_id"] = profile_id
        resp = await self._request("GET", "/v1/alerts/deliveries", params=params)
        return resp.json()

    # -- Compliance monitoring (Enterprise) ------------------------------------

    async def add_monitored_entity(
        self, entity_identifier: str, identifier_type: str = "afm", label: str | None = None
    ) -> dict:
        """Add an entity to monitor. Enterprise only."""
        payload: dict = {"entity_identifier": entity_identifier, "identifier_type": identifier_type}
        if label:
            payload["label"] = label
        resp = await self._request("POST", "/v1/monitor/entities", json=payload)
        return resp.json()

    async def list_monitored_entities(self, limit: int = 20, offset: int = 0) -> dict:
        """List monitored entities."""
        resp = await self._request("GET", "/v1/monitor/entities", params={"limit": limit, "offset": offset})
        return resp.json()

    async def remove_monitored_entity(self, entity_id: str) -> dict:
        """Remove (deactivate) a monitored entity."""
        resp = await self._request("DELETE", f"/v1/monitor/entities/{entity_id}")
        return resp.json()

    async def list_entity_events(
        self,
        entity_id: str | None = None,
        event_type: str | None = None,
        since: str | None = None,
        limit: int = 20,
        offset: int = 0,
    ) -> dict:
        """List events for monitored entities."""
        params: dict[str, str | int] = {"limit": limit, "offset": offset}
        if entity_id:
            params["entity_id"] = entity_id
        if event_type:
            params["event_type"] = event_type
        if since:
            params["since"] = since
        resp = await self._request("GET", "/v1/monitor/events", params=params)
        return resp.json()

    async def acknowledge_event(self, event_id: str) -> dict:
        """Acknowledge a monitoring event."""
        resp = await self._request("POST", f"/v1/monitor/events/{event_id}/acknowledge")
        return resp.json()

    async def entity_health_report(self, entity_id: str) -> dict:
        """Trigger a health report for a monitored entity."""
        resp = await self._request("GET", f"/v1/monitor/entities/{entity_id}/report")
        return resp.json()

    # -- Pagination iterators --------------------------------------------------

    async def search_iter(self, query: str, page_size: int = 100) -> AsyncIterator[Document]:
        """Iterate through all search results, auto-paginating."""
        offset = 0
        while True:
            resp = await self.search(query, limit=page_size, offset=offset)
            for doc in resp.results:
                yield doc
            offset += page_size
            if offset >= resp.total:
                break

    async def documents_iter(
        self, source: str, page_size: int = 100, since: str | None = None
    ) -> AsyncIterator[Document]:
        """Iterate through all documents from a source, auto-paginating."""
        offset = 0
        while True:
            resp = await self.documents(source, limit=page_size, offset=offset, since=since)
            for doc in resp.documents:
                yield doc
            offset += page_size
            if offset >= resp.total:
                break

    # -- Source-specific typed clients ------------------------------------------

    @property
    def ted(self) -> AsyncTedSource:
        from monopigi.sources import AsyncTedSource

        return AsyncTedSource(self)

    @property
    def diavgeia(self) -> AsyncDiavgeiaSource:
        from monopigi.sources import AsyncDiavgeiaSource

        return AsyncDiavgeiaSource(self)

    @property
    def elstat(self) -> AsyncElstatSource:
        from monopigi.sources import AsyncElstatSource

        return AsyncElstatSource(self)

    @property
    def rae(self) -> AsyncRaeSource:
        from monopigi.sources import AsyncRaeSource

        return AsyncRaeSource(self)

    @property
    def data_gov_gr(self) -> AsyncDataGovGrSource:
        from monopigi.sources import AsyncDataGovGrSource

        return AsyncDataGovGrSource(self)

    @property
    def mitos(self) -> AsyncMitosSource:
        from monopigi.sources import AsyncMitosSource

        return AsyncMitosSource(self)

    @property
    def kimdis(self) -> AsyncKimdisSource:
        from monopigi.sources import AsyncKimdisSource

        return AsyncKimdisSource(self)

    @property
    def geodata(self) -> AsyncGeodataSource:
        from monopigi.sources import AsyncGeodataSource

        return AsyncGeodataSource(self)

    async def close(self) -> None:
        await self._client.aclose()

    async def __aenter__(self) -> AsyncMonopigiClient:
        return self

    async def __aexit__(self, *args: object) -> None:
        await self.close()
