"""Async-first root client.

This module defines :class:`~relationalai.client.client.Client`, the async-first root client
returned by :func:`~relationalai.client.connect`.
"""

from __future__ import annotations

import inspect
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from .core import ClientCore

from relationalai.util.docutils import include_in_docs

__include_in_docs__ = True

if TYPE_CHECKING:
    from relationalai.config.config import Config
    from relationalai.services.jobs.client import JobsClient
    from relationalai.services.reasoners.client import ReasonersClient


@include_in_docs
@dataclass
class Client:
    """Async-first root client.

    Use this client when you are already writing async code:
    - construct via `await relationalai.client.connect(...)` or `await from_session(...)`
    - close via `await client.aclose()` or `async with ...`

    Key differences vs `ClientSync`:
    - `.reasoners` returns an async `ReasonersClient` (methods are `async def`, so you `await`)

    Logging/representation:
    - sensitive fields (config, auth tokens, sessions/transports) are excluded from `repr(...)`
    """

    core: ClientCore = field(repr=False)
    _reasoners: "ReasonersClient | None" = field(default=None, init=False, repr=False, compare=False)
    _jobs: "JobsClient | None" = field(default=None, init=False, repr=False, compare=False)

    @include_in_docs
    async def aclose(self) -> None:
        """Close the client (best-effort).

        Returns
        -------
        None
            Always returns `None`.
        """
        # Close any instantiated service clients first so they can release resources
        # (or mark themselves as closed) before transports are torn down.
        if self._reasoners is not None:
            try:
                await self._reasoners.aclose()
            except Exception:
                # Best-effort close: don't mask caller exceptions during shutdown.
                pass
        if self._jobs is not None:
            try:
                await self._jobs.aclose()
            except Exception:
                pass

        if self.core._http is not None:
            # Best-effort close for requests-backed sessions.
            try:
                session = getattr(self.core._http, "session", None)
                close = getattr(session, "close", None)
                if callable(close):
                    close()
            except Exception:
                pass

        if self.core._http_async is not None:
            session = getattr(self.core._http_async, "session", None)
            close = getattr(session, "close", None)
            if callable(close):
                out = close()
                if inspect.isawaitable(out):
                    await out

    async def __aenter__(self) -> "Client":
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:  # noqa: ANN001
        await self.aclose()

    @include_in_docs
    @property
    def reasoners(self) -> "ReasonersClient":
        """Reasoners service client (cached, async).

        Returns a `ReasonersClient` whose public API is async-first (awaitable methods).

        Returns
        -------
        ReasonersClient
            A cached async-first reasoners client.
        """
        if self._reasoners is not None:
            return self._reasoners
        from relationalai.services.reasoners.wiring import build_reasoners_client

        self._reasoners = build_reasoners_client(self)
        return self._reasoners

    @include_in_docs
    def get_reasoners(self) -> "ReasonersClient":
        """Return the reasoners service client.

        This is the method form of :attr:`reasoners`, provided so reference docs can
        render an explicit API entry.

        Returns
        -------
        ReasonersClient
            A cached async-first reasoners client.
        """
        return self.reasoners

    @include_in_docs
    @property
    def jobs(self) -> "JobsClient":
        """Jobs service client (cached, async).

        Returns
        -------
        JobsClient
            A cached async-first jobs client.
        """
        if self._jobs is not None:
            return self._jobs
        from relationalai.services.jobs.wiring import build_jobs_client

        self._jobs = build_jobs_client(self)
        return self._jobs

    @include_in_docs
    def get_jobs(self) -> "JobsClient":
        """Return the jobs service client.

        This is the method form of :attr:`jobs`, provided so reference docs can
        render an explicit API entry.

        Returns
        -------
        JobsClient
            A cached async-first jobs client.
        """
        return self.jobs

    # ---------------------------------------------------------------------
    # Common state accessors (delegated to `ClientCore`).
    # ---------------------------------------------------------------------
    @include_in_docs
    @property
    def config(self) -> "Config":
        """Return the active configuration.

        Returns
        -------
        Config
            The config used by this client.
        """
        return self.core.config

    @include_in_docs
    @property
    def execution_metrics(self) -> object | None:
        """Execution metrics from the active transport (read-only diagnostic handle)."""
        return self.core.execution_metrics

    @include_in_docs
    @property
    def app_name(self) -> str | None:
        """Return the app name used for SQL routing.

        Returns
        -------
        str | None
            The app name, if known.
        """
        return self.core.app_name

    @app_name.setter
    def app_name(self, v: str | None) -> None:
        self.core.app_name = v

