"""Snowpark-backed SQL transport with middleware.

This module defines `SqlClient`, the SDK's SQL execution adapter used by:
- the root client (`ClientCore.sql_executor`)
- service gateways that execute stored procedures / queries

Key goals
---------
- Provide a **stable execution interface** (`collect(...)`) independent of Snowpark version quirks.
- Apply the shared **runtime middleware pipeline** (logging/metrics/retry/error mapping).
- Avoid leaking sensitive session/config details in logs (`repr` hides the session).
"""

from __future__ import annotations

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

from relationalai.runtime.execution import Executor, SqlRequest
from relationalai.runtime.pipeline import build_executor

from ..errors.handlers import ErrorHandlerChain, ExecContext
from .types import SnowparkSessionLike


@dataclass
class SqlClient:
    """SQL executor (Snowpark-backed) with middleware pipeline.

    This is a thin wrapper around Snowpark `Session.sql(...).collect()` that:
    - constructs a runtime `SqlRequest` with operation + metadata
    - routes it through the middleware pipeline built by `runtime.pipeline.build_executor`
    - feature-detects Snowpark's parameter binding signature once at init time
    """

    # Sessions can include connection details; never show in `repr`.
    session: SnowparkSessionLike = field(repr=False)
    middlewares: list[Any] | None = None
    error_handlers: ErrorHandlerChain | None = None
    execution_metrics: Any | None = None

    def __post_init__(self) -> None:
        session = self.session
        supports_kw_params: bool

        # Snowpark's `Session.sql(...)` parameter binding API has differed across versions:
        # some accept `params=` keyword, others only accept positional params.
        #
        # We feature-detect once (at construction time) to avoid a try/except TypeError
        # on every call in the hot path.
        try:
            sig = inspect.signature(session.sql)
            supports_kw_params = "params" in sig.parameters
        except (TypeError, ValueError):
            # Fall back to a single dry call that does not execute (no `.collect()`).
            try:
                session.sql("select 1", params=[])
            except TypeError:
                supports_kw_params = False
            else:
                supports_kw_params = True

        class _Base(Executor[SqlRequest, list[Any]]):
            def execute(_self: object, req: SqlRequest) -> list[Any]:
                params = req.params
                if params is None:
                    return session.sql(req.statement).collect()
                if supports_kw_params:
                    return session.sql(req.statement, params=params).collect()
                return session.sql(req.statement, params).collect()

        def _ctx_factory(req: SqlRequest) -> ExecContext:
            return ExecContext(statement=req.statement, params=req.params, operation=req.operation, meta=req.meta)

        self._executor = build_executor(
            base=_Base(),
            middlewares=self.middlewares,
            error_handlers=self.error_handlers or ErrorHandlerChain(),
            ctx_factory=_ctx_factory,
        )

    def collect(
        self,
        statement: str,
        params: list[Any] | None = None,
        *,
        operation: str,
        meta: dict[str, Any] | None = None,
    ) -> list[Any]:
        """Execute SQL and return collected rows.

        - **statement**: SQL statement or stored procedure call.
        - **params**: positional bind parameters (optional).
        - **operation**: a stable operation name used by middleware/logging/metrics.
        - **meta**: optional structured metadata (e.g. app/schema/object) for diagnostics.
        """
        req = SqlRequest(operation=operation, statement=statement, params=params if params else None, meta=meta or {})
        return self._executor.execute(req)

