"""Client wiring: build transports from `Config`.

This module contains the low-level constructors used by `ClientCore` to lazily create:
- Snowpark SQL transport (`SqlClient`)
- Direct Access HTTP transports (`HttpClient` / `AsyncHttpClient`)

It also composes the runtime middleware chain based on `config.execution`.

Direct Access authentication is handled via a token handler derived from the
active Snowflake connection, request-time auth headers are injected by
middleware, and service endpoint discovery is delegated to the SQL transport.
"""

from __future__ import annotations

import importlib
from typing import TYPE_CHECKING
from relationalai.config.config_fields import ExecutionConfig
from relationalai.config.direct_access import DA_SUPPORTED_AUTHENTICATORS

from relationalai.runtime.retry_classification import (
    _default_retryable_http_error,
    _default_retryable_http_error_async,
    _is_retryable_snowflake_error,
)

from ..transports.http import AsyncHttpClient, HttpClient
from ..middleware.compose import compose_middlewares
from ..transports.sql import SqlClient

if TYPE_CHECKING:
    from relationalai.config.config import Config


_LOGGER_NAME = "relationalai.client.execution"


def _is_expected_snowflake_connection_lookup_error(e: Exception) -> bool:
    """Return True when Snowflake connection lookup is genuinely unavailable."""
    msg = str(e or "").strip().lower()

    if isinstance(e, ModuleNotFoundError):
        missing = (getattr(e, "name", None) or "").strip()
        return missing == "snowflake" or missing.startswith("snowflake.snowpark")

    if isinstance(e, ValueError):
        return (
            ("multiple connections available" in msg)
            or ("connection '" in msg and "not found" in msg)
            or ("default_connection" in msg and "not found" in msg)
            or ("config must have at least one connection" in msg)
            or ("is not of type snowflakeconnection" in msg)
        )

    return False
def default_config() -> Config:
    """Single config initialization point for the client module."""
    from relationalai.config import create_config

    return create_config()


def effective_execution_cfg(cfg: Config) -> ExecutionConfig:
    """Return the execution config.

    Note: this repo intentionally does not support per-service execution overrides.
    """
    return cfg.execution


def _prepend_direct_access_auth_middleware(
    middlewares: list[object],
    *,
    cfg: Config,
    token: str | None,
    connection_name: str | None,
) -> list[object]:
    """Build and prepend the Direct Access auth middleware for an HTTP transport.

    When `token` is provided, DA requests use that static token directly.
    Otherwise we resolve the selected Snowflake connection and construct the
    matching `TokenHandler`, which mints/refreshes ingress tokens per request.

    The auth middleware must wrap the transport before the generic execution
    middlewares so every outgoing DA request already has auth headers attached.
    """
    from relationalai.auth import TokenHandler
    from relationalai.config.connections import SnowflakeConnection
    from relationalai.runtime.middleware.da_auth import DirectAccessAuthMiddleware

    if token:
        auth_middleware = DirectAccessAuthMiddleware(static_token=token)
    else:
        conn = cfg.get_connection(SnowflakeConnection, name=connection_name)
        handler = TokenHandler.from_snowflake_connection(conn)
        auth_middleware = DirectAccessAuthMiddleware(token_handler=handler)

    # Keep auth outermost so retries/logging/metrics see the fully authenticated request.
    return [auth_middleware, *middlewares]


def validate_direct_access_auth_config(
    cfg: Config, *, token: str | None, connection_name: str | None = None
) -> None:
    """Raise early when DA backend is configured with an unsupported authenticator."""
    if not cfg.direct_access or token:
        return
    try:
        from relationalai.auth import DirectAccessConfigError
        from relationalai.config.connections import SnowflakeConnection

        conn = cfg.get_connection(SnowflakeConnection, name=connection_name)
    except Exception as e:
        if _is_expected_snowflake_connection_lookup_error(e):
            return
        raise

    authenticator = str(getattr(conn, "authenticator", "") or "")
    if authenticator in DA_SUPPORTED_AUTHENTICATORS:
        return
    supported = ", ".join(sorted(DA_SUPPORTED_AUTHENTICATORS))
    raise DirectAccessConfigError(
        "Direct Access backend requires a Snowflake connection with one of these authenticators: "
        f"{supported}. Got authenticator={authenticator!r}. "
        "Use `token=...` to bypass connection-based Direct Access auth middleware."
    )


def build_sql_from_config(cfg: Config, *, connection_name: str | None = None) -> tuple[SqlClient | None, str | None]:
    """Build the SQL transport (Snowpark), returning (sql_client, app_name)."""
    try:
        from relationalai.config.connections import SnowflakeConnection

        conn = cfg.get_connection(SnowflakeConnection, name=connection_name)
        session = conn.get_session()
        app_name = conn.rai_app_name
    except Exception as e:
        # Return (None, None) only when SQL is genuinely unavailable:
        # - Snowpark isn't installed, or
        # - no usable Snowflake connection is configured/selected.
        # For everything else (e.g. auth/SSO failures), re-raise so callers see the real error.
        if _is_expected_snowflake_connection_lookup_error(e):
            return None, None

        raise

    exec_cfg = effective_execution_cfg(cfg)
    middlewares, metrics = compose_middlewares(
        exec_cfg,
        is_retryable=lambda e, _req: _is_retryable_snowflake_error(e),
        logger_name=_LOGGER_NAME,
    )
    sql = SqlClient(session=session, middlewares=middlewares, execution_metrics=metrics)
    return sql, app_name


def build_http_sync(
    cfg: Config, *, token: str | None, connection_name: str | None = None
) -> HttpClient | None:
    """Build the sync HTTP transport (requests).

    `token` overrides DA auth middleware when provided.
    """
    if not cfg.direct_access:
        return None
    validate_direct_access_auth_config(cfg, token=token, connection_name=connection_name)
    exec_cfg = effective_execution_cfg(cfg)

    sync_middlewares, sync_metrics = compose_middlewares(
        exec_cfg,
        is_retryable=_default_retryable_http_error,
        logger_name=_LOGGER_NAME,
    )
    sync_middlewares = _prepend_direct_access_auth_middleware(
        list(sync_middlewares or []), cfg=cfg, token=token, connection_name=connection_name
    )

    requests = importlib.import_module("requests")
    http_session = requests.Session()
    http_session.headers.update({"Accept": "application/json"})
    return HttpClient(session=http_session, middlewares=sync_middlewares, execution_metrics=sync_metrics)


async def build_http_async(
    cfg: Config, *, token: str | None, connection_name: str | None = None
) -> AsyncHttpClient | None:
    """Build the async HTTP transport (aiohttp).

    `token` overrides DA auth middleware when provided.
    """
    if not cfg.direct_access:
        return None
    validate_direct_access_auth_config(cfg, token=token, connection_name=connection_name)
    exec_cfg = effective_execution_cfg(cfg)

    async_middlewares, async_metrics = compose_middlewares(
        exec_cfg,
        is_retryable=_default_retryable_http_error_async,
        logger_name=_LOGGER_NAME,
    )

    try:
        aiohttp = importlib.import_module("aiohttp")
    except ModuleNotFoundError:
        return None

    async_middlewares = _prepend_direct_access_auth_middleware(
        list(async_middlewares or []), cfg=cfg, token=token, connection_name=connection_name
    )

    session = aiohttp.ClientSession(headers={"Accept": "application/json"})
    return AsyncHttpClient(session=session, middlewares=async_middlewares, execution_metrics=async_metrics)

