"""Backends (gateways) for the Reasoners service.

Gateways are backend adapters: they know endpoints / SQL shapes and how to translate
responses into stable domain models.

They are intentionally not responsible for:
- creating sessions/transports (owned by the root client core)
- choosing a backend (done in `reasoners/wiring.py`)
- exposing a user-facing API (owned by `ReasonersClient` / `ReasonersClientSync`)
"""

from __future__ import annotations

import inspect
import json
from dataclasses import dataclass
from typing import Any, Protocol, TYPE_CHECKING, TypeAlias, cast

from relationalai.client.transports.http import AsyncHttpClient, HttpClient
from relationalai.services._shared.da_request import BaseUrlProvider, request_async, request_sync
from relationalai.services._shared.da_headers import mk_da_headers
from relationalai.services._shared.sql_rows import row_to_mapping

from . import constants as c
from .errors import (
    ReasonerAuthError,
    ReasonerConflictError,
    ReasonerHttpError,
    ReasonerNotFoundError,
    ReasonerServerError,
    ReasonerValidationException,
)
from .models import Operation, Reasoner, ReasonerType, reasoner_from_row
from .operations import _op_from_reasoner, _op_id, _parse_op_id, _wait_for_operation_async, _wait_for_operation_sync
from .util import build_where

if TYPE_CHECKING:
    import requests

    HttpResponse: TypeAlias = requests.Response
else:
    HttpResponse: TypeAlias = Any


class _SqlExecutorLike(Protocol):
    def collect(
        self,
        statement: str,
        params: list[Any] | None = None,
        *,
        operation: str,
        meta: dict[str, Any],
    ) -> list[Any]: ...


class _ReasonersCfgLike(Protocol):
    @property
    def show_all_sizes(self) -> bool: ...


class _ConfigLike(Protocol):
    @property
    def reasoners(self) -> _ReasonersCfgLike: ...


def _supported_reasoner_sizes(config: _ConfigLike) -> list[str]:
    cloud_sizes = c.REASONER_SIZES_AWS
    if config.reasoners.show_all_sizes:
        return list(c.REASONER_SIZES_INTERNAL) + list(cloud_sizes)
    return cloud_sizes


def _http_error_message(status: int, payload: object) -> str:
    if isinstance(payload, dict):
        return str(payload.get("message") or payload.get("error") or json.dumps(payload))
    return str(payload) or f"HTTP {status}"


def _raise_for_status_content(op: str, status: int, payload: object) -> None:
    """Map non-200 status/payload pairs to stable Reasoners exceptions."""
    if int(status) == 200:
        return

    msg = _http_error_message(int(status), payload)
    if int(status) in (401, 403):
        raise ReasonerAuthError(f"Direct Access request unauthorized (status={status}). {msg}")
    if int(status) == 404:
        raise ReasonerNotFoundError(msg)
    if int(status) == 409:
        raise ReasonerConflictError(msg)
    if int(status) == 400:
        raise ReasonerValidationException(operation=op, details=msg, cause=Exception(msg))
    if int(status) == 429 or 500 <= int(status) <= 599:
        raise ReasonerHttpError(status=int(status), message=msg, operation=op)
    raise ReasonerServerError(f"Direct Access request failed (status={status}). {msg}")


def _raise_for_http(op: str, resp: HttpResponse) -> None:
    """Map non-200 responses to stable Reasoners exceptions."""
    if resp.status_code == 200:
        return
    try:
        payload = resp.json()
    except (ValueError, json.JSONDecodeError, TypeError):
        payload = resp.text or f"HTTP {resp.status_code}"
    _raise_for_status_content(op, int(resp.status_code), payload)


def _parse_da_list_reasoners(
    content: dict[str, Any],
    *,
    state: str | None,
    reasoner_name: str | None,
    reasoner_type: str | None,
    reasoner_size: str | None,
    created_by: str | None,
) -> list[Reasoner]:
    items = content.get("engines")
    if items is None:
        items = []
    elif not isinstance(items, list):
        raise ReasonerServerError("Direct Access list returned malformed payload: 'engines' must be a list.")

    out: list[Reasoner] = []
    for e in items:
        if state is not None and str(e.get("status", "")).upper() != state.upper():
            continue
        if reasoner_name is not None and reasoner_name.upper() not in str(e.get("name", "")).upper():
            continue
        etype_raw = (e.get("type") or c.DEFAULT_REASONER_TYPE)
        etype = _da_to_public_reasoner_type(str(etype_raw))
        if reasoner_type is not None and ReasonerType.normalize(str(etype)) != ReasonerType.normalize(reasoner_type):
            continue
        if reasoner_size is not None and e.get("size") != reasoner_size:
            continue
        if created_by is not None and created_by.upper() not in str(e.get("created_by", "")).upper():
            continue

        out.append(
            Reasoner(
                name=e["name"],
                type=str(etype),
                id=e.get("id"),
                size=e.get("size"),
                state=e.get("status"),
                created_by=e.get("created_by"),
                created_on=e.get("created_on"),
                updated_on=e.get("updated_on"),
                auto_suspend_mins=e.get("auto_suspend_mins"),
                suspends_at=e.get("suspends_at"),
                settings=e.get("settings"),
            )
        )
    return sorted(out, key=lambda r: r.name)


# ---------------------------------------------------------------------------
# Direct Access route/capability policy (used by HttpGateway / AsyncHttpGateway)
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class _DaOperationSpec:
    method: str | None = None
    endpoint: str | None = None
    allow_404: bool = False
    sql_fallback: bool = False


_DA_OPERATION_SPECS: dict[str, _DaOperationSpec] = {
    "list": _DaOperationSpec(method="GET", endpoint=c.REASONERS_DA_BASE_PATH),
    "get": _DaOperationSpec(
        method="GET",
        endpoint=f"{c.REASONERS_DA_BASE_PATH}/{{engine_type}}/{{engine_name}}",
        allow_404=True,
    ),
    "create": _DaOperationSpec(method="POST", endpoint=f"{c.REASONERS_DA_BASE_PATH}/{{engine_type}}"),
    "delete": _DaOperationSpec(method="DELETE", endpoint=f"{c.REASONERS_DA_BASE_PATH}/{{engine_type}}/{{engine_name}}"),
    "suspend": _DaOperationSpec(
        method="POST",
        endpoint=f"{c.REASONERS_DA_BASE_PATH}/{{engine_type}}/{{engine_name}}/suspend",
    ),
    "resume": _DaOperationSpec(
        method="POST",
        endpoint=f"{c.REASONERS_DA_BASE_PATH}/{{engine_type}}/{{engine_name}}/resume_async",
    ),
    "alter_auto_suspend_mins": _DaOperationSpec(sql_fallback=True),
}


def _da_spec(operation: str) -> _DaOperationSpec:
    try:
        return _DA_OPERATION_SPECS[operation]
    except KeyError as e:
        raise KeyError(f"Unknown reasoners DA operation spec: {operation}") from e


def _da_path_params(operation: str, *, reasoner_type: str | None = None, reasoner_name: str | None = None) -> dict[str, str] | None:
    spec = _da_spec(operation)
    if spec.endpoint is None:
        return None

    params: dict[str, str] = {}
    if "{engine_type}" in spec.endpoint:
        if reasoner_type is None:
            raise ValueError(f"reasoner_type is required for DA operation '{operation}'")
        params["engine_type"] = _public_to_da_reasoner_type_path(ReasonerType.normalize(reasoner_type))
    if "{engine_name}" in spec.endpoint:
        if reasoner_name is None:
            raise ValueError(f"reasoner_name is required for DA operation '{operation}'")
        params["engine_name"] = reasoner_name
    return params or None


def _should_fallback_to_sql(method_name: str) -> bool:
    """Return True only for operations explicitly backed by SQL in v1.

    v0 effectively knew the Direct Access capability surface up front through an
    endpoint registry. Keep v1 similarly explicit: only operations we intentionally
    model as SQL-only should use the SQL gateway. DA-supported operations should
    surface their own HTTP/domain errors instead of guessing from 404 text.
    """
    return _da_spec(method_name).sql_fallback


def _public_to_da_reasoner_type(public_type: str) -> str:
    """Map public reasoner types (LOGIC/PRESCRIPTIVE/PREDICTIVE) to DA reasoner types."""
    t = ReasonerType.normalize(public_type)
    if t == c.REASONER_TYPE_PREDICTIVE:
        return "ML"
    if t == c.REASONER_TYPE_PRESCRIPTIVE:
        return "SOLVER"
    return t


def _public_to_da_reasoner_type_path(public_type: str) -> str:
    """Map public reasoner types to lowercase DA URL path segments."""
    return _public_to_da_reasoner_type(public_type).lower()


def _da_to_public_reasoner_type(da_type: str) -> str:
    """Map DA reasoner types to public reasoner types."""
    u = str(da_type or "").strip().upper()
    if u == "ML":
        return c.REASONER_TYPE_PREDICTIVE
    if u in {"SOLVER", "OPT"}:
        return c.REASONER_TYPE_PRESCRIPTIVE
    if u in {c.REASONER_TYPE_LOGIC, c.REASONER_TYPE_PREDICTIVE, c.REASONER_TYPE_PRESCRIPTIVE}:
        return u
    return u


@dataclass
class SqlGateway:
    """Reasoners gateway implemented via Snowflake SQL."""

    executor: _SqlExecutorLike
    config: _ConfigLike
    app: str
    schema: str = c.DEFAULT_REASONERS_SCHEMA

    def _execute(self, statement: str, params: list[Any] | None, *, operation: str) -> list[Any]:
        """Execute a SQL statement via the injected `SqlClient`."""
        return self.executor.collect(
            statement,
            params if params else None,
            operation=operation,
            meta={"app": self.app, "schema": self.schema, "params": 0 if not params else len(params)},
        )

    def list(
        self,
        *,
        state: str | None = None,
        reasoner_name: str | None = None,
        reasoner_type: str | None = None,
        reasoner_size: str | None = None,
        created_by: str | None = None,
    ) -> list[Reasoner]:
        """List reasoners via the SQL app schema, optionally filtering."""
        where_clause, params = build_where(
            [
                ("STATUS = ?", state.upper() if state else None),
                ("UPPER(NAME) LIKE ?", f"%{reasoner_name.upper()}%" if reasoner_name else None),
                ("UPPER(TYPE) = ?", reasoner_type.upper() if reasoner_type else None),
                ("SIZE = ?", reasoner_size),
                ("UPPER(CREATED_BY) LIKE ?", f"%{created_by.upper()}%" if created_by else None),
            ]
        )
        where_suffix = f" {where_clause}" if where_clause else ""

        statement = f"""
            SELECT
                NAME, TYPE, ID, SIZE, STATUS, CREATED_BY, CREATED_ON, UPDATED_ON,
                AUTO_SUSPEND_MINS, SUSPENDS_AT, SETTINGS
            FROM {self.app}.{self.schema}.reasoners{where_suffix}
            ORDER BY NAME ASC;
        """

        rows = self._execute(statement, params if params else [], operation="list")
        if not rows:
            return []
        out: list[Reasoner] = []
        for r in rows:
            d = row_to_mapping(r)
            out.append(reasoner_from_row(d))
        return out

    def get(self, reasoner_type: str, reasoner_name: str) -> Reasoner | None:
        """Get a reasoner by `(type, name)` via SQL, returning `None` if missing."""
        statement = f"""
            SELECT
                NAME, TYPE, ID, SIZE, STATUS, CREATED_BY, CREATED_ON, UPDATED_ON,
                VERSION, AUTO_SUSPEND_MINS, SUSPENDS_AT, SETTINGS
            FROM {self.app}.{self.schema}.reasoners
            WHERE UPPER(NAME) = UPPER(?) AND UPPER(TYPE) = ?;
        """
        params = [reasoner_name, reasoner_type.upper()]
        rows = self._execute(statement, params, operation="get")
        if not rows:
            return None
        r0 = rows[0]
        d = row_to_mapping(r0)
        return reasoner_from_row(d)

    def create(
        self,
        reasoner_type: str,
        reasoner_name: str,
        *,
        reasoner_size: str | None = None,
        auto_suspend_mins: int | None = None,
        await_storage_vacuum: bool | None = None,
        settings: dict[str, Any] | None = None,
    ) -> Operation:
        """Create a reasoner (async proc) and return a client-side `Operation` handle."""
        name = reasoner_name
        type_val = ReasonerType.normalize(reasoner_type)
        size = reasoner_size

        if size is None:
            raise ReasonerValidationException(operation="create", details="size is required for reasoner creation")

        payload: dict[str, Any] = {}
        if settings:
            payload["settings"] = dict(settings)
        if auto_suspend_mins is not None:
            payload["auto_suspend_mins"] = auto_suspend_mins
        if await_storage_vacuum is not None:
            payload["await_storage_vacuum"] = await_storage_vacuum

        proc = "create_reasoner_async"
        stmt = f"call {self.app}.{self.schema}.{proc}(?, ?, ?, PARSE_JSON(?));"
        self._execute(stmt, [type_val, name, size, json.dumps(payload)], operation=proc)
        t = ReasonerType.normalize(reasoner_type)
        return Operation(
            id=_op_id("create", reasoner_type=t, reasoner_name=reasoner_name),
            status=c.STATUS_PENDING,
            raw={"kind": "create", "name": reasoner_name, "type": t},
        )

    def delete(self, reasoner_type: str, reasoner_name: str) -> None:
        """Delete a reasoner via SQL stored procedure."""
        stmt = f"call {self.app}.{self.schema}.delete_reasoner(?, ?);"
        self._execute(stmt, [ReasonerType.normalize(reasoner_type), reasoner_name], operation="delete_reasoner")

    def alter_auto_suspend_mins(self, reasoner_type: str, reasoner_name: str, auto_suspend_mins: int) -> None:
        """Alter the auto-suspend timeout for an existing reasoner via SQL stored procedure."""
        t = ReasonerType.normalize(reasoner_type)
        stmt = f"call {self.app}.{self.schema}.alter_reasoner_auto_suspend_mins(?, ?, ?);"
        self._execute(stmt, [t, reasoner_name, auto_suspend_mins], operation="alter_reasoner_auto_suspend_mins")

    def suspend(self, reasoner_type: str, reasoner_name: str) -> None:
        """Suspend a reasoner via SQL stored procedure."""
        t = ReasonerType.normalize(reasoner_type)
        stmt = f"call {self.app}.{self.schema}.suspend_reasoner(?, ?);"
        self._execute(stmt, [t, reasoner_name], operation="suspend_reasoner")

    def resume(self, reasoner_type: str, reasoner_name: str) -> Operation:
        """Resume a reasoner (async proc) and return a client-side `Operation` handle."""
        t = ReasonerType.normalize(reasoner_type)
        stmt = f"call {self.app}.{self.schema}.resume_reasoner_async(?, ?);"
        self._execute(stmt, [t, reasoner_name], operation="resume_reasoner_async")
        return Operation(
            id=_op_id("resume", reasoner_type=t, reasoner_name=reasoner_name),
            status=c.STATUS_PENDING,
            raw={"kind": "resume", "name": reasoner_name, "type": t},
        )

    def sizes(self) -> list[str]:
        """Return supported reasoner sizes for this backend/config."""
        return _supported_reasoner_sizes(self.config)

    def validate_size(self, size: str | None) -> tuple[bool, list[str]]:
        """Validate a size, returning `(is_valid, allowed_sizes)`."""
        if size is None:
            return True, []
        sizes = self.sizes()
        if size not in sizes:
            return False, sizes
        return True, []

    def get_operation(self, operation_id: str) -> Operation:
        """Derive an operation status from current reasoner state (SQL backend)."""
        kind, rtype, rname = _parse_op_id(operation_id)
        r = self.get(rtype, rname)
        return _op_from_reasoner(kind, operation_id, r)

    def wait_for_operation(self, operation_id: str, *, timeout_s: int = 900) -> Operation:
        """Block until the derived operation completes (or fails on timeout)."""
        return _wait_for_operation_sync(self.get_operation, operation_id, timeout_s=timeout_s)


@dataclass
class HttpGateway:
    """Reasoners gateway backed by Direct Access HTTP endpoints (sync requests).

    When ``sql_fallback`` is provided, operations that the DA HTTP API does not
    support (detected via :func:`_should_fallback_to_sql`) are transparently
    retried against the SQL gateway.
    """

    config: _ConfigLike
    base_url_provider: BaseUrlProvider
    http: HttpClient
    sql_fallback: SqlGateway | None = None

    def _headers(self) -> dict[str, str] | None:
        # Stable Direct Access headers (auth is injected via middleware).
        return mk_da_headers(cast(Any, self.config), gi_setup_skipped=False)

    def _request(
        self,
        *,
        operation: str,
        method: str,
        endpoint: str,
        payload: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        path_params: dict[str, str] | None = None,
        query_params: dict[str, str] | None = None,
        meta: dict[str, Any] | None = None,
    ) -> Any:
        """Perform a single HTTP request via the injected sync `HttpClient`."""
        return request_sync(
            http=self.http,
            operation=operation,
            method=method,
            base_url_provider=self.base_url_provider,
            endpoint=endpoint,
            path_params=path_params,
            query_params=query_params,
            payload=payload,
            headers=headers,
            meta=meta,
        )

    # -- internal fallback helper --

    def _fallback(self, method_name: str, err: Exception, *args: Any, **kwargs: Any) -> Any:
        """Delegate only explicitly SQL-backed operations to the SQL gateway."""
        fb = self.sql_fallback
        if fb is not None and _should_fallback_to_sql(method_name):
            return getattr(fb, method_name)(*args, **kwargs)
        raise err

    # -- gateway methods --

    def list(
        self,
        *,
        state: str | None = None,
        reasoner_name: str | None = None,
        reasoner_type: str | None = None,
        reasoner_size: str | None = None,
        created_by: str | None = None,
    ) -> list[Reasoner]:
        """List reasoners using Direct Access HTTP (sync)."""
        try:
            return self._list_http(
                state=state,
                reasoner_name=reasoner_name,
                reasoner_type=reasoner_type,
                reasoner_size=reasoner_size,
                created_by=created_by,
            )
        except Exception as e:
            return self._fallback(
                "list",
                e,
                state=state,
                reasoner_name=reasoner_name,
                reasoner_type=reasoner_type,
                reasoner_size=reasoner_size,
                created_by=created_by,
            )

    def _list_http(
        self,
        *,
        state: str | None = None,
        reasoner_name: str | None = None,
        reasoner_type: str | None = None,
        reasoner_size: str | None = None,
        created_by: str | None = None,
    ) -> list[Reasoner]:
        op = "list"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        resp = self._request(operation=op, method=spec.method, endpoint=spec.endpoint, headers=self._headers())
        _raise_for_http(op, resp)
        return _parse_da_list_reasoners(
            resp.json() or {},
            state=state,
            reasoner_name=reasoner_name,
            reasoner_type=reasoner_type,
            reasoner_size=reasoner_size,
            created_by=created_by,
        )

    def get(self, reasoner_type: str, reasoner_name: str) -> Reasoner | None:
        """Get a reasoner by `(type, name)` using Direct Access HTTP (sync)."""
        try:
            return self._get_http(reasoner_type, reasoner_name)
        except Exception as e:
            return self._fallback("get", e, reasoner_type, reasoner_name)

    def _get_http(self, reasoner_type: str, reasoner_name: str) -> Reasoner | None:
        op = "get"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        t_public = ReasonerType.normalize(reasoner_type)
        resp = self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=t_public, reasoner_name=reasoner_name),
        )
        if spec.allow_404 and resp.status_code == 404:
            return None
        _raise_for_http(op, resp)
        e = resp.json() or {}
        if not e:
            return None
        etype = _da_to_public_reasoner_type(str(e.get("type") or t_public))
        return Reasoner(
            name=e["name"],
            type=etype,
            id=e.get("id"),
            size=e.get("size"),
            state=e.get("status"),
            created_by=e.get("created_by"),
            created_on=e.get("created_on"),
            updated_on=e.get("updated_on"),
            version=e.get("version"),
            auto_suspend_mins=e.get("auto_suspend_mins"),
            suspends_at=e.get("suspends_at"),
            settings=e.get("settings"),
        )

    def create(
        self,
        reasoner_type: str,
        reasoner_name: str,
        *,
        reasoner_size: str | None = None,
        auto_suspend_mins: int | None = None,
        await_storage_vacuum: bool | None = None,
        settings: dict[str, Any] | None = None,
    ) -> Operation:
        """Create a reasoner using Direct Access HTTP (sync) and return an `Operation`."""
        try:
            return self._create_http(
                reasoner_type,
                reasoner_name,
                reasoner_size=reasoner_size,
                auto_suspend_mins=auto_suspend_mins,
                await_storage_vacuum=await_storage_vacuum,
                settings=settings,
            )
        except Exception as e:
            return self._fallback(
                "create",
                e,
                reasoner_type,
                reasoner_name,
                reasoner_size=reasoner_size,
                auto_suspend_mins=auto_suspend_mins,
                await_storage_vacuum=await_storage_vacuum,
                settings=settings,
            )

    def _create_http(
        self,
        reasoner_type: str,
        reasoner_name: str,
        *,
        reasoner_size: str | None = None,
        auto_suspend_mins: int | None = None,
        await_storage_vacuum: bool | None = None,
        settings: dict[str, Any] | None = None,
    ) -> Operation:
        op = "create"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        t_public = ReasonerType.normalize(reasoner_type)
        if reasoner_size is None:
            raise ReasonerValidationException(operation=op, details="size is required for Direct Access reasoner creation")

        payload: dict[str, Any] = {"name": reasoner_name, "size": reasoner_size}
        if settings:
            payload["settings"] = dict(settings)
        if auto_suspend_mins is not None:
            payload["auto_suspend_mins"] = auto_suspend_mins
        if await_storage_vacuum is not None:
            payload["await_storage_vacuum"] = await_storage_vacuum

        resp = self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=t_public),
            payload=payload,
        )
        _raise_for_http(op, resp)
        return Operation(
            id=_op_id("create", reasoner_type=t_public, reasoner_name=reasoner_name),
            status=c.STATUS_PENDING,
            raw={"kind": "create", "name": reasoner_name, "type": t_public},
        )

    def delete(self, reasoner_type: str, reasoner_name: str) -> None:
        """Delete a reasoner using Direct Access HTTP (sync)."""
        try:
            return self._delete_http(reasoner_type, reasoner_name)
        except Exception as e:
            return self._fallback("delete", e, reasoner_type, reasoner_name)

    def _delete_http(self, reasoner_type: str, reasoner_name: str) -> None:
        op = "delete"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        resp = self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=reasoner_type, reasoner_name=reasoner_name),
        )
        _raise_for_http(op, resp)

    def alter_auto_suspend_mins(self, reasoner_type: str, reasoner_name: str, auto_suspend_mins: int) -> None:
        """Alter the auto-suspend timeout for an existing reasoner (falls back to SQL)."""
        return self._fallback(
            "alter_auto_suspend_mins",
            ReasonerHttpError(status=501, message="not implemented in HTTP API", operation="alter_auto_suspend_mins"),
            reasoner_type, reasoner_name, auto_suspend_mins,
        )

    def suspend(self, reasoner_type: str, reasoner_name: str) -> None:
        """Suspend a reasoner using Direct Access HTTP (sync)."""
        try:
            return self._suspend_http(reasoner_type, reasoner_name)
        except Exception as e:
            return self._fallback("suspend", e, reasoner_type, reasoner_name)

    def _suspend_http(self, reasoner_type: str, reasoner_name: str) -> None:
        op = "suspend"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        resp = self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=reasoner_type, reasoner_name=reasoner_name),
        )
        _raise_for_http(op, resp)

    def resume(self, reasoner_type: str, reasoner_name: str) -> Operation:
        """Resume a reasoner using Direct Access HTTP (sync) and return an `Operation`."""
        try:
            return self._resume_http(reasoner_type, reasoner_name)
        except Exception as e:
            return self._fallback("resume", e, reasoner_type, reasoner_name)

    def _resume_http(self, reasoner_type: str, reasoner_name: str) -> Operation:
        op = "resume"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        t_public = ReasonerType.normalize(reasoner_type)
        resp = self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=t_public, reasoner_name=reasoner_name),
        )
        _raise_for_http(op, resp)
        return Operation(
            id=_op_id("resume", reasoner_type=t_public, reasoner_name=reasoner_name),
            status=c.STATUS_PENDING,
            raw={"kind": "resume", "name": reasoner_name, "type": t_public},
        )

    def sizes(self) -> list[str]:
        """Return supported sizes for Direct Access backend (sync)."""
        return _supported_reasoner_sizes(self.config)

    def validate_size(self, size: str | None) -> tuple[bool, list[str]]:
        """Validate a size, returning `(is_valid, allowed_sizes)`."""
        if size is None:
            return True, []
        sizes = self.sizes()
        if size not in sizes:
            return False, sizes
        return True, []

    def get_operation(self, operation_id: str) -> Operation:
        """Derive an operation status from current reasoner state (HTTP sync backend)."""
        kind, rtype, rname = _parse_op_id(operation_id)
        r = self.get(rtype, rname)
        return _op_from_reasoner(kind, operation_id, r)

    def wait_for_operation(self, operation_id: str, *, timeout_s: int = 900) -> Operation:
        """Block until the derived operation completes (or fails on timeout)."""
        return _wait_for_operation_sync(self.get_operation, operation_id, timeout_s=timeout_s)


@dataclass
class AsyncHttpGateway:
    """Async-native Direct Access gateway (aiohttp).

    When ``sql_fallback`` is provided (an async-compatible gateway), operations that
    the DA HTTP API does not support are transparently retried against SQL.
    """

    config: _ConfigLike
    base_url_provider: BaseUrlProvider
    http: AsyncHttpClient
    sql_fallback: Any | None = None  # async ReasonersGateway (typically SyncToAsyncGatewayAdapter wrapping SqlGateway)

    def _headers(self) -> dict[str, str] | None:
        return mk_da_headers(cast(Any, self.config), gi_setup_skipped=False)

    async def _request(
        self,
        *,
        operation: str,
        method: str,
        endpoint: str,
        payload: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        path_params: dict[str, str] | None = None,
        query_params: dict[str, str] | None = None,
        allow_404: bool = False,
    ) -> dict[str, Any] | None:
        """Perform an HTTP request and return the decoded JSON payload (or `None` for allow_404)."""
        status, payload_out = await request_async(
            http=self.http,
            operation=operation,
            method=method,
            base_url_provider=self.base_url_provider,
            endpoint=endpoint,
            path_params=path_params,
            query_params=query_params,
            payload=payload,
            headers=headers,
            allow_404=allow_404,
        )

        if allow_404 and int(status) == 404:
            return None
        _raise_for_status_content(operation, int(status), payload_out)
        if payload_out is None:
            return None
        if isinstance(payload_out, dict):
            return payload_out
        return {"value": payload_out}

    # -- internal fallback helper --

    async def _fallback(self, method_name: str, err: Exception, *args: Any, **kwargs: Any) -> Any:
        """Delegate only explicitly SQL-backed operations to the SQL gateway."""
        fb = self.sql_fallback
        if fb is not None and _should_fallback_to_sql(method_name):
            target = getattr(fb, method_name)
            out = target(*args, **kwargs)
            if inspect.isawaitable(out):
                return await out
            return out
        raise err

    async def aclose(self) -> None:
        """Close the underlying aiohttp session (if present)."""
        session = getattr(self.http, "session", None)
        close = getattr(session, "close", None)
        if callable(close):
            out = close()
            if inspect.isawaitable(out):
                await cast(Any, out)

    # -- gateway methods --

    async def list(
        self,
        *,
        state: str | None = None,
        reasoner_name: str | None = None,
        reasoner_type: str | None = None,
        reasoner_size: str | None = None,
        created_by: str | None = None,
    ) -> list[Reasoner]:
        """List reasoners using Direct Access HTTP (async)."""
        try:
            return await self._list_http(
                state=state,
                reasoner_name=reasoner_name,
                reasoner_type=reasoner_type,
                reasoner_size=reasoner_size,
                created_by=created_by,
            )
        except Exception as e:
            return await self._fallback(
                "list",
                e,
                state=state,
                reasoner_name=reasoner_name,
                reasoner_type=reasoner_type,
                reasoner_size=reasoner_size,
                created_by=created_by,
            )

    async def _list_http(
        self,
        *,
        state: str | None = None,
        reasoner_name: str | None = None,
        reasoner_type: str | None = None,
        reasoner_size: str | None = None,
        created_by: str | None = None,
    ) -> list[Reasoner]:
        op = "list"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        content = await self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
        ) or {}
        return _parse_da_list_reasoners(
            content,
            state=state,
            reasoner_name=reasoner_name,
            reasoner_type=reasoner_type,
            reasoner_size=reasoner_size,
            created_by=created_by,
        )

    async def get(self, reasoner_type: str, reasoner_name: str) -> Reasoner | None:
        """Get a reasoner by `(type, name)` using Direct Access HTTP (async)."""
        try:
            return await self._get_http(reasoner_type, reasoner_name)
        except Exception as e:
            return await self._fallback("get", e, reasoner_type, reasoner_name)

    async def _get_http(self, reasoner_type: str, reasoner_name: str) -> Reasoner | None:
        op = "get"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        t_public = ReasonerType.normalize(reasoner_type)
        e = await self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=t_public, reasoner_name=reasoner_name),
            allow_404=spec.allow_404,
        )
        if e is None:
            return None
        if not isinstance(e, dict) or not e:
            return None
        etype = _da_to_public_reasoner_type(str(e.get("type") or t_public))
        return Reasoner(
            name=e["name"],
            type=etype,
            id=e.get("id"),
            size=e.get("size"),
            state=e.get("status"),
            created_by=e.get("created_by"),
            created_on=e.get("created_on"),
            updated_on=e.get("updated_on"),
            version=e.get("version"),
            auto_suspend_mins=e.get("auto_suspend_mins"),
            suspends_at=e.get("suspends_at"),
            settings=e.get("settings"),
        )

    async def create(
        self,
        reasoner_type: str,
        reasoner_name: str,
        *,
        reasoner_size: str | None = None,
        auto_suspend_mins: int | None = None,
        await_storage_vacuum: bool | None = None,
        settings: dict[str, Any] | None = None,
    ) -> Operation:
        """Create a reasoner using Direct Access HTTP (async) and return an `Operation`."""
        try:
            return await self._create_http(
                reasoner_type,
                reasoner_name,
                reasoner_size=reasoner_size,
                auto_suspend_mins=auto_suspend_mins,
                await_storage_vacuum=await_storage_vacuum,
                settings=settings,
            )
        except Exception as e:
            return await self._fallback(
                "create",
                e,
                reasoner_type,
                reasoner_name,
                reasoner_size=reasoner_size,
                auto_suspend_mins=auto_suspend_mins,
                await_storage_vacuum=await_storage_vacuum,
                settings=settings,
            )

    async def _create_http(
        self,
        reasoner_type: str,
        reasoner_name: str,
        *,
        reasoner_size: str | None = None,
        auto_suspend_mins: int | None = None,
        await_storage_vacuum: bool | None = None,
        settings: dict[str, Any] | None = None,
    ) -> Operation:
        op = "create"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        t_public = ReasonerType.normalize(reasoner_type)
        if reasoner_size is None:
            raise ReasonerValidationException(operation=op, details="size is required for Direct Access reasoner creation")

        payload: dict[str, Any] = {"name": reasoner_name, "size": reasoner_size}
        if settings:
            payload["settings"] = dict(settings)
        if auto_suspend_mins is not None:
            payload["auto_suspend_mins"] = auto_suspend_mins
        if await_storage_vacuum is not None:
            payload["await_storage_vacuum"] = await_storage_vacuum

        await self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=t_public),
            payload=payload,
        )
        return Operation(
            id=_op_id("create", reasoner_type=t_public, reasoner_name=reasoner_name),
            status=c.STATUS_PENDING,
            raw={"kind": "create", "name": reasoner_name, "type": t_public},
        )

    async def delete(self, reasoner_type: str, reasoner_name: str) -> None:
        """Delete a reasoner using Direct Access HTTP (async)."""
        try:
            return await self._delete_http(reasoner_type, reasoner_name)
        except Exception as e:
            return await self._fallback("delete", e, reasoner_type, reasoner_name)

    async def _delete_http(self, reasoner_type: str, reasoner_name: str) -> None:
        op = "delete"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        await self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=reasoner_type, reasoner_name=reasoner_name),
        )

    async def alter_auto_suspend_mins(self, reasoner_type: str, reasoner_name: str, auto_suspend_mins: int) -> None:
        """Alter the auto-suspend timeout for an existing reasoner (falls back to SQL)."""
        return await self._fallback(
            "alter_auto_suspend_mins",
            ReasonerHttpError(status=501, message="not implemented in HTTP API", operation="alter_auto_suspend_mins"),
            reasoner_type, reasoner_name, auto_suspend_mins,
        )

    async def suspend(self, reasoner_type: str, reasoner_name: str) -> None:
        """Suspend a reasoner using Direct Access HTTP (async)."""
        try:
            return await self._suspend_http(reasoner_type, reasoner_name)
        except Exception as e:
            return await self._fallback("suspend", e, reasoner_type, reasoner_name)

    async def _suspend_http(self, reasoner_type: str, reasoner_name: str) -> None:
        op = "suspend"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        await self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=reasoner_type, reasoner_name=reasoner_name),
        )

    async def resume(self, reasoner_type: str, reasoner_name: str) -> Operation:
        """Resume a reasoner using Direct Access HTTP (async) and return an `Operation`."""
        try:
            return await self._resume_http(reasoner_type, reasoner_name)
        except Exception as e:
            return await self._fallback("resume", e, reasoner_type, reasoner_name)

    async def _resume_http(self, reasoner_type: str, reasoner_name: str) -> Operation:
        op = "resume"
        spec = _da_spec(op)
        assert spec.method is not None and spec.endpoint is not None
        t_public = ReasonerType.normalize(reasoner_type)
        await self._request(
            operation=op,
            method=spec.method,
            endpoint=spec.endpoint,
            headers=self._headers(),
            path_params=_da_path_params(op, reasoner_type=t_public, reasoner_name=reasoner_name),
        )
        return Operation(
            id=_op_id("resume", reasoner_type=t_public, reasoner_name=reasoner_name),
            status=c.STATUS_PENDING,
            raw={"kind": "resume", "name": reasoner_name, "type": t_public},
        )

    async def sizes(self) -> list[str]:
        """Return supported sizes for Direct Access backend (async)."""
        return _supported_reasoner_sizes(self.config)

    async def validate_size(self, size: str | None) -> tuple[bool, list[str]]:
        """Validate a size, returning `(is_valid, allowed_sizes)`."""
        if size is None:
            return True, []
        sizes = await self.sizes()
        if size not in sizes:
            return False, sizes
        return True, []

    async def get_operation(self, operation_id: str) -> Operation:
        """Derive an operation status from current reasoner state (HTTP async backend)."""
        kind, rtype, rname = _parse_op_id(operation_id)
        r = await self.get(rtype, rname)
        return _op_from_reasoner(kind, operation_id, r)

    async def wait_for_operation(self, operation_id: str, *, timeout_s: int = 900) -> Operation:
        """Wait until the derived operation completes (or fails on timeout)."""
        return await _wait_for_operation_async(self.get_operation, operation_id, timeout_s=timeout_s)

