"""Root client entrypoints.

This package exposes the primary connection entrypoints for PyRel:

- :func:`connect` / :class:`~relationalai.client.client.Client` for async-first usage
- :func:`connect_sync` / :class:`~relationalai.client.client_sync.ClientSync` for sync usage
- :func:`from_session` / :func:`from_session_sync` for constructing clients from an existing
  Snowpark/DBAPI-like session

Service APIs are exposed off the root clients:

- :attr:`~relationalai.client.client.Client.reasoners` and :attr:`~relationalai.client.client_sync.ClientSync.reasoners`
- :attr:`~relationalai.client.client.Client.jobs` and :attr:`~relationalai.client.client_sync.ClientSync.jobs`

The root clients lazily create transports (SQL/HTTP) on first use.
"""

from __future__ import annotations

from functools import lru_cache
from importlib import import_module
from typing import Any, TYPE_CHECKING, TypeVar

from .client import Client
from .client_sync import ClientSync
from .core import ClientCore
from relationalai.util.asyncio import raise_if_running_event_loop
from relationalai.util.docutils import include_in_docs

# Public re-exports for service clients.
#
# We intentionally avoid *eagerly* importing these service modules at import-time:
# some services indirectly import `relationalai.client.errors...`, which imports
# `relationalai.client`, creating circular-import chains (observed in unit tests).
#
# Instead, we provide:
# - **type-checking only** imports (so IDEs/pyright understand names), and
# - **lazy** runtime re-exports via `__getattr__`.
if TYPE_CHECKING:
    from relationalai.services.jobs.client import JobsClient
    from relationalai.services.jobs.client_sync import JobsClientSync
    from relationalai.services.reasoners.client import ReasonersClient
    from relationalai.services.reasoners.client_sync import ReasonersClientSync


_EXPORTS: dict[str, tuple[str, str]] = {
    "JobsClient": ("relationalai.services.jobs.client", "JobsClient"),
    "JobsClientSync": ("relationalai.services.jobs.client_sync", "JobsClientSync"),
    "ReasonersClient": ("relationalai.services.reasoners.client", "ReasonersClient"),
    "ReasonersClientSync": ("relationalai.services.reasoners.client_sync", "ReasonersClientSync"),
}


@lru_cache(maxsize=None)
def __getattr__(name: str) -> Any:  # pragma: no cover - exercised indirectly by imports
    spec = _EXPORTS.get(name)
    if spec is None:
        raise AttributeError(name)
    module_name, attr_name = spec
    return getattr(import_module(module_name), attr_name)


def __dir__() -> list[str]:  # pragma: no cover - trivial
    return sorted(list(globals().keys()) + list(_EXPORTS.keys()))


__all__ = [
    "Client",
    "ClientSync",
    "ClientCore",
    "connect",
    "connect_sync",
    "from_session",
    "from_session_sync",
    "JobsClient",
    "JobsClientSync",
    "ReasonersClient",
    "ReasonersClientSync",
]

__include_in_docs__ = True

if TYPE_CHECKING:
    from relationalai.config.config import Config


_ClientT = TypeVar("_ClientT", Client, ClientSync)


def _get_config(config: Config | None) -> Config:
    if config is not None:
        return config
    # Lazy import: keep `import relationalai.client` lightweight.
    from .wiring.wiring import default_config

    return default_config()


def _connect_lazy(
    client_type: type[_ClientT], *, config: Config, connection_name: str | None = None, token: str | None = None
) -> _ClientT:
    # Lazy by default: do not create transports here.
    # Construct positionally for compatibility with stricter doc/type tooling.
    core = ClientCore(config, None, None, None, None, connection_name, token)
    return client_type(core=core)


def _from_session_client(
    client_type: type[_ClientT], *, session: Any, config: Config, token: str | None = None, app_name: str | None = None
) -> _ClientT:
    # Session is already provided; create SQL transport immediately.
    from relationalai.runtime.retry_classification import _is_retryable_snowflake_error

    from .middleware.compose import compose_middlewares
    from .transports.sql import SqlClient

    exec_cfg = config.execution
    middlewares, metrics = compose_middlewares(
        exec_cfg,
        is_retryable=lambda e, _req: _is_retryable_snowflake_error(e),
        logger_name="relationalai.client.execution",
    )
    sql = SqlClient(session=session, middlewares=middlewares, execution_metrics=metrics)
    # Construct positionally for compatibility with stricter doc/type tooling.
    core = ClientCore(config, sql, None, None, app_name, None, token)
    return client_type(core=core)


@include_in_docs
async def connect(
    *, config: "Config | None" = None, connection_name: str | None = None, token: str | None = None
) -> Client:
    """Async-first entrypoint returning a `Client`.

    Use this from async code (you can `await` it). The returned `Client`:
    - exposes async lifecycle (`await client.aclose()` / `async with ...`)
    - exposes async service clients (e.g. `client.reasoners` -> `ReasonersClient`)

    Transport creation is **lazy**: no network calls are made by `connect(...)` itself.
    Transports are created on first use (SQL/HTTP/services).

    If you are in synchronous code, prefer `connect_sync()` which returns `ClientSync`.

    Parameters
    ----------
    config:
        Optional config to use. When omitted, a default config is loaded.
    connection_name:
        Optional named connection to use from the config.
    token:
        Optional RAI token override.

    Returns
    -------
    Client
        An async-first root client. Use `async with await connect(...) as client:` or
        `client = await connect(...); await client.aclose()`.
    """
    cfg = _get_config(config)
    return _connect_lazy(Client, config=cfg, connection_name=connection_name, token=token)


@include_in_docs
def connect_sync(
    *, config: "Config | None" = None, connection_name: str | None = None, token: str | None = None
) -> ClientSync:
    """Synchronous entrypoint returning a `ClientSync`.

    Use this from synchronous code. The returned `ClientSync`:
    - exposes sync lifecycle (`client.close()` / `with ...`)
    - exposes synchronous service clients (e.g. `client.reasoners` -> `ReasonersClientSync`)

    Transport creation is **lazy**: no network calls are made by `connect_sync(...)` itself.
    Transports are created on first use (SQL/HTTP/services).

    This function raises if called while an event loop is already running in the
    current thread (use `await connect(...)` from async code instead).

    Parameters
    ----------
    config:
        Optional config to use. When omitted, a default config is loaded.
    connection_name:
        Optional named connection to use from the config.
    token:
        Optional RAI token override.

    Returns
    -------
    ClientSync
        A synchronous root client. Use `with connect_sync(...) as client:` or
        `client = connect_sync(...); client.close()`.
    """
    raise_if_running_event_loop("connect_sync", use_in_async="`connect(...)` from async code")
    cfg = _get_config(config)
    return _connect_lazy(ClientSync, config=cfg, connection_name=connection_name, token=token)


@include_in_docs
async def from_session(
    *, session: Any, config: "Config | None" = None, token: str | None = None, app_name: str | None = None
) -> Client:
    """Construct an async `Client` from an existing session.

    Parameters
    ----------
    session:
        A Snowpark- or DBAPI-like session/connection.
    config:
        Optional config to use for defaults and middleware settings.
    token:
        Optional RAI token override.
    app_name:
        Optional app name override.

    Returns
    -------
    Client
        An async-first root client backed by the provided session.
    """
    cfg = _get_config(config)
    return _from_session_client(Client, session=session, config=cfg, token=token, app_name=app_name)


@include_in_docs
def from_session_sync(
    *, session: Any, config: "Config | None" = None, token: str | None = None, app_name: str | None = None
) -> ClientSync:
    """Construct a sync `ClientSync` from an existing Snowpark session.

    Raises if called from a thread that already has a running event loop.

    Parameters
    ----------
    session:
        A Snowpark- or DBAPI-like session/connection.
    config:
        Optional config to use for defaults and middleware settings.
    token:
        Optional RAI token override.
    app_name:
        Optional app name override.

    Returns
    -------
    ClientSync
        A synchronous root client backed by the provided session.
    """
    raise_if_running_event_loop("from_session_sync", use_in_async="`from_session(...)` from async code")
    cfg = _get_config(config)
    return _from_session_client(ClientSync, session=session, config=cfg, token=token, app_name=app_name)


__all__ = [
    "Client",
    "ClientSync",
    "connect",
    "connect_sync",
    "from_session",
    "from_session_sync",
    "JobsClient",
    "JobsClientSync",
    "ReasonersClient",
    "ReasonersClientSync",
]

