"""Jobs routing layer (Jobs + LOGIC Transactions facade).

This single module contains both the public jobs router and the concrete LOGIC
transaction gateway implementations. The goal is to keep the public API in
`client.jobs` stable while containing the "transactions as jobs" complexity in
one place.
"""

from __future__ import annotations

import asyncio
import json
from dataclasses import dataclass
from typing import Any, Protocol, TYPE_CHECKING

from relationalai.client.transports.http import AsyncHttpClient, HttpClient
from relationalai.client.transports.sql import SqlClient
from relationalai.services._shared.artifact_info import coerce_da_artifact_results, uppercase_keys
from relationalai.services._shared.da_headers import mk_da_headers
from relationalai.services._shared.da_request import BaseUrlProvider
from relationalai.services._shared.sql_rows import row_first_value, row_get, row_to_mapping
from relationalai.services.constants import REASONER_TYPE_LOGIC

if TYPE_CHECKING:
    from relationalai.config.config import Config

from .constants import TRANSACTIONS_DA_BASE_PATH
from .da_http import request_json_async, request_json_sync
from .errors import JobInvalidRequestError, JobServerError
from .gateways import JobsGateway, JobsGatewaySync
from .models import JobEvents, JobRecord, job_events_from_value, logic_txn_job_from_row

_TXN_ID_KEYS: tuple[str, ...] = ("id", "txn_id", "ID", "TXN_ID")
_TXN_WRAPPER_KEYS: tuple[str, ...] = ("transaction", "txn", "job", "data", "result", "value", "payload")


def _is_logic(reasoner_type: str) -> bool:
    return str(reasoner_type).strip().upper() == REASONER_TYPE_LOGIC


# ---------------------------------------------------------------------------
# Transactions gateways (surfaced through the Jobs facade for LOGIC).
# ---------------------------------------------------------------------------


def _txn_query_params(cfg: "Config", *, payload: dict[str, Any]) -> dict[str, Any]:
    # Keep the v1 Jobs facade on the legacy transaction-create semantics even in
    # Direct Access mode. The GI/object execution path changes database selection
    # behavior and expects orchestration that the old exec stack handled, but this
    # facade deliberately does not.
    _ = cfg, payload
    return {"use_graph_index": "false"}


def _txn_create_body(*, reasoner_name: str, payload: dict[str, Any], timeout_mins: int | None) -> dict[str, Any]:
    body = dict(payload)
    body["engine_name"] = reasoner_name
    body["abort_detached"] = bool(payload.get("abort_detached") or False)
    if timeout_mins is not None and "timeout_mins" not in body:
        body["timeout_mins"] = int(timeout_mins)
    return body


def _as_str(v: Any) -> str:
    if isinstance(v, str):
        return v.strip()
    if v is None:
        return ""
    try:
        return str(v).strip()
    except Exception:
        return ""


def _unwrap_txn_payload(out: Any) -> Any:
    if not isinstance(out, dict):
        return out
    for key in _TXN_WRAPPER_KEYS:
        val = out.get(key)
        if isinstance(val, dict) and val:
            return val
    return out


def _extract_txn_id(out: Any) -> str:
    if isinstance(out, str):
        return out.strip()
    if isinstance(out, dict):
        for key in _TXN_ID_KEYS:
            value = _as_str(out.get(key))
            if value:
                return value
        inner = _unwrap_txn_payload(out)
        if isinstance(inner, dict) and inner is not out:
            for key in _TXN_ID_KEYS:
                value = _as_str(inner.get(key))
                if value:
                    return value
        for key in _TXN_WRAPPER_KEYS:
            raw_value = out.get(key)
            if isinstance(raw_value, (dict, list, tuple, set)):
                continue
            value = _as_str(raw_value)
            if value:
                return value
    return ""


def _require_txn_id(out: Any) -> str:
    txn_id = _extract_txn_id(out)
    if not txn_id:
        if isinstance(out, dict):
            msg = _as_str(out.get("message") or out.get("error"))
            if msg:
                raise RuntimeError(f"Transaction creation failed (no id in response). {msg}")
        raise RuntimeError(f"Transaction creation did not return an ID. response={out!r}")
    return txn_id


def _txn_events_query_params(continuation_token: str) -> dict[str, Any]:
    # DA first-page polling historically treats both "" and "0" as "start from
    # the beginning"; normalize to the empty token so SQL and HTTP follow the
    # same convention and callers/tests can pass either sentinel.
    token = "" if str(continuation_token) == "0" else str(continuation_token or "")
    return {"continuation_token": token}


def _txn_headers(cfg: "Config", *, gi_setup_skipped: bool) -> dict[str, str]:
    return mk_da_headers(cfg, gi_setup_skipped=gi_setup_skipped)


def _txn_job_record(out: Any) -> JobRecord | None:
    payload = _unwrap_txn_payload(out)
    if not isinstance(payload, dict) or not payload:
        return None
    return logic_txn_job_from_row(payload)


def _txn_events(out: Any, *, txn_id: str, continuation_token: str, stream_name: str) -> JobEvents:
    _ = txn_id, continuation_token, stream_name
    return job_events_from_value(out)


def _txn_artifacts(out: Any) -> dict[str, dict[str, Any]]:
    out2 = uppercase_keys(out) if isinstance(out, dict) else {}
    return coerce_da_artifact_results(out2)


def _txn_problems(out: Any) -> dict[str, Any]:
    return out if isinstance(out, dict) else {}


def _txn_default_headers(headers: dict[str, str] | None) -> dict[str, str]:
    # Transaction DA requests are JSON by default, but callers can still pass
    # explicit headers when they need to extend or override that baseline.
    return headers or {"Content-Type": "application/json"}


def _txn_create_request_kwargs(
    cfg: "Config",
    *,
    reasoner_name: str,
    payload: dict[str, Any],
    timeout_mins: int | None,
) -> dict[str, Any]:
    return {
        "operation": "create",
        "method": "POST",
        "endpoint": TRANSACTIONS_DA_BASE_PATH,
        "query_params": _txn_query_params(cfg, payload=payload),
        "json_body": _txn_create_body(reasoner_name=reasoner_name, payload=payload, timeout_mins=timeout_mins),
        "headers": _txn_headers(cfg, gi_setup_skipped=bool(payload.get("gi_setup_skipped") or False)),
    }


def _txn_get_request_kwargs(cfg: "Config", txn_id: str) -> dict[str, Any]:
    return {
        "operation": "get",
        "method": "GET",
        "endpoint": f"{TRANSACTIONS_DA_BASE_PATH}/{{txn_id}}",
        "path_params": {"txn_id": txn_id},
        "headers": _txn_headers(cfg, gi_setup_skipped=False),
        "allow_404": True,
    }


def _txn_events_request_kwargs(cfg: "Config", txn_id: str, continuation_token: str, stream_name: str) -> dict[str, Any]:
    return {
        "operation": "events",
        "method": "GET",
        "endpoint": f"{TRANSACTIONS_DA_BASE_PATH}/{{txn_id}}/events/{{stream_name}}",
        "path_params": {"txn_id": txn_id, "stream_name": stream_name},
        "query_params": _txn_events_query_params(continuation_token),
        "headers": _txn_headers(cfg, gi_setup_skipped=False),
    }


def _txn_artifacts_request_kwargs(cfg: "Config", txn_id: str) -> dict[str, Any]:
    return {
        "operation": "artifacts",
        "method": "GET",
        "endpoint": f"{TRANSACTIONS_DA_BASE_PATH}/{{txn_id}}/artifacts",
        "path_params": {"txn_id": txn_id},
        "headers": _txn_headers(cfg, gi_setup_skipped=False),
    }


def _txn_problems_request_kwargs(cfg: "Config", txn_id: str) -> dict[str, Any]:
    return {
        "operation": "problems",
        "method": "GET",
        "endpoint": f"{TRANSACTIONS_DA_BASE_PATH}/{{txn_id}}/problems",
        "path_params": {"txn_id": txn_id},
        "headers": _txn_headers(cfg, gi_setup_skipped=False),
    }


@dataclass
class TxnSqlGateway:
    """Transactions gateway implemented via SQL (table + stored procs/functions)."""

    executor: "SqlClient"
    app: str
    config: "Config"

    def _collect(self, statement: str, params: list[Any] | None, *, operation: str) -> list[Any]:
        return self.executor.collect(
            statement,
            params if params else None,
            operation=operation,
            meta={
                "app": self.app,
                "schema": "api",
                "object": "transactions",
                "params": 0 if not params else len(params),
            },
        )

    def get(self, id: str) -> JobRecord | None:
        stmt = f"""
            SELECT
                id, database_name, engine_name, state, abort_reason, read_only,
                created_by, created_on, finished_at, duration
            FROM {self.app}.api.transactions
            WHERE id = ?
            LIMIT 1;
        """
        rows = self._collect(stmt, [id], operation="get")
        if not rows:
            return None
        return logic_txn_job_from_row(rows[0])

    def list(
        self,
        *,
        job_id: str | None = None,
        state: str | None = None,
        name: str | None = None,
        created_by: str | None = None,
        only_active: bool = False,
        all_users: bool = False,
        limit: int | None = 100,
    ) -> list[JobRecord]:
        # Reuse the existing implementation by importing from the old module at runtime is not desired here;
        # keep logic local to this merged file.
        where: list[str] = []
        params: list[Any] = []

        if job_id:
            where.append("id = ?")
            params.append(job_id)

        if state:
            where.append("state = ?")
            params.append(state)
        elif only_active:
            where.append("state in ('CREATED', 'RUNNING', 'PENDING')")

        if name:
            where.append("LOWER(engine_name) = ?")
            params.append(str(name).lower())
        if not all_users and created_by is not None:
            where.append("LOWER(created_by) = ?")
            params.append(str(created_by).lower())

        where_clause = f"WHERE {' AND '.join(where)}" if where else ""
        limit_clause = ""
        if limit is not None:
            limit_int = int(limit)
            if limit_int <= 0:
                return []
            limit_clause = " LIMIT ?"
            params.append(limit_int)

        stmt = f"""
            SELECT
                id, database_name, engine_name, state, abort_reason, read_only,
                created_by, created_on, finished_at, duration
            FROM {self.app}.api.transactions
            {where_clause}
            ORDER BY created_on DESC{limit_clause};
        """
        rows = self._collect(stmt, params, operation="list")
        return [logic_txn_job_from_row(r) for r in rows]

    def create(self, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        # For LOGIC, the Jobs facade uses this parameter as the reasoner name.
        reasoner = str(reasoner_name).strip()
        if not reasoner:
            raise JobInvalidRequestError("Transaction create requires a non-empty reasoner name.")

        db_any = payload.get("dbname") or payload.get("database") or payload.get("database_name")
        dbname = str(db_any).strip() if isinstance(db_any, str) else ""
        if not dbname:
            raise JobInvalidRequestError("Transaction create requires payload['dbname'] (database name).")

        q_any = payload.get("query") or payload.get("raw_code") or payload.get("code")
        query = str(q_any).strip() if isinstance(q_any, str) else ""
        if not query:
            raise JobInvalidRequestError("Transaction create requires payload['query'] (raw code).")

        inputs_any = payload.get("v1_inputs")
        if inputs_any is None:
            inputs_any = payload.get("inputs")
        inputs = inputs_any if isinstance(inputs_any, dict) else {}

        ro_any = payload.get("readonly")
        if ro_any is None:
            ro_any = payload.get("read_only")
        readonly = True if ro_any is None else bool(ro_any)

        nowait_durable = bool(payload.get("nowait_durable") or False)
        lang_any = payload.get("language")
        language = str(lang_any) if isinstance(lang_any, str) and lang_any else "rel"

        hdrs = payload.get("headers")
        headers = hdrs if isinstance(hdrs, dict) else {}

        to_mins_any = payload.get("timeout_mins")
        payload_timeout = int(to_mins_any) if isinstance(to_mins_any, int) else None
        tm = timeout_mins if timeout_mins is not None else payload_timeout

        # For the v1 Jobs facade we intentionally use the **legacy** `exec_async_v2`
        # signature, even when `use_graph_index` is enabled.
        #
        # Reason: the GI/object payload form changes database selection semantics and
        # expects GI orchestration (setup/retry) which is handled in the upstream exec
        # stack (v0) but is out of scope for this jobs facade.
        inputs_json = json.dumps(inputs)
        headers_json = json.dumps(headers)
        abort_detached = bool(payload.get("abort_detached") or False)
        if tm is not None:
            stmt = f"call {self.app}.api.exec_async_v2(?, ?, ?, PARSE_JSON(?), ?, ?, ?, ?, PARSE_JSON(?), ?);"
            params = [
                dbname,
                reasoner,
                query,
                inputs_json,
                readonly,
                nowait_durable,
                language,
                int(tm),
                headers_json,
                abort_detached,
            ]
        else:
            stmt = f"call {self.app}.api.exec_async_v2(?, ?, ?, PARSE_JSON(?), ?, ?, ?, PARSE_JSON(?), ?);"
            params = [dbname, reasoner, query, inputs_json, readonly, nowait_durable, language, headers_json, abort_detached]

        try:
            rows = self._collect(stmt, params, operation="create")
        except Exception as e:
            # Backwards compatibility with older sql-lib versions where `exec_async_v2`
            # does not yet accept the `abort_detached` argument.
            msg = str(e).lower()
            if "exec_async_v2" not in msg or (
                "wrong number of arguments" not in msg and "too many arguments" not in msg and "argument" not in msg
            ):
                raise
            if tm is not None:
                stmt = f"call {self.app}.api.exec_async_v2(?, ?, ?, PARSE_JSON(?), ?, ?, ?, ?, PARSE_JSON(?));"
                params = [dbname, reasoner, query, inputs_json, readonly, nowait_durable, language, int(tm), headers_json]
            else:
                stmt = f"call {self.app}.api.exec_async_v2(?, ?, ?, PARSE_JSON(?), ?, ?, ?, PARSE_JSON(?));"
                params = [dbname, reasoner, query, inputs_json, readonly, nowait_durable, language, headers_json]
            rows = self._collect(stmt, params, operation="create")
        if not rows:
            raise RuntimeError("Transaction creation returned no results (expected an ID row).")
        txn_id = row_get(rows[0], "ID", "id", "TXN_ID", "txn_id")
        if not isinstance(txn_id, str):
            txn_id = str(txn_id) if txn_id is not None else ""
        if not txn_id:
            raise RuntimeError("Transaction creation did not return an ID.")
        return txn_id

    def events(self, id: str, continuation_token: str = "", *, stream_name: str = "progress") -> JobEvents:
        if stream_name and stream_name != "progress":
            raise JobInvalidRequestError("SQL backend for transactions only supports stream_name='progress'.")
        stmt = f"select {self.app}.api.get_own_transaction_events(?, ?);"
        tok_in = str(continuation_token or "")
        tok = "" if tok_in == "0" else tok_in
        rows = self._collect(stmt, [id, tok], operation="events")
        if not rows:
            return JobEvents(events=[], continuation_token=None)
        val = row_first_value(rows[0])
        return job_events_from_value(val)

    def cancel(self, id: str, *, all_users: bool = False) -> None:
        proc = "CANCEL_TRANSACTION" if all_users else "CANCEL_OWN_TRANSACTION"
        stmt = f"call {self.app}.api.{proc}(?);"
        self._collect(stmt, [id], operation="cancel")
        return None

    def artifacts(self, id: str) -> dict[str, Any]:
        stmt = f"call {self.app}.api.get_own_transaction_artifacts(?);"
        rows = self._collect(stmt, [id], operation="artifacts")
        if not rows:
            return {}
        d = row_to_mapping(rows[0])
        return d if isinstance(d, dict) else {}


@dataclass
class TxnHttpGateway:
    """Transactions gateway backed by Direct Access HTTP endpoints (sync)."""

    config: "Config"
    base_url_provider: BaseUrlProvider
    http: HttpClient

    def _request(
        self,
        *,
        operation: str,
        method: str,
        endpoint: str,
        path_params: dict[str, Any] | None = None,
        query_params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        allow_404: bool = False,
    ) -> Any:
        _status, content = request_json_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=json_body,
            headers=_txn_default_headers(headers),
            allow_404=allow_404,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Transactions",
        )
        return content

    def create(self, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        out = self._request(**_txn_create_request_kwargs(self.config, reasoner_name=reasoner_name, payload=payload, timeout_mins=timeout_mins))
        return _require_txn_id(out)

    def get(self, id: str) -> JobRecord | None:
        out = self._request(**_txn_get_request_kwargs(self.config, id))
        return _txn_job_record(out)

    def events(self, id: str, continuation_token: str = "", *, stream_name: str = "progress") -> JobEvents:
        out = self._request(**_txn_events_request_kwargs(self.config, id, continuation_token, stream_name))
        return _txn_events(out, txn_id=id, continuation_token=continuation_token, stream_name=stream_name)

    def artifacts(self, id: str) -> dict[str, dict[str, Any]]:
        out = self._request(**_txn_artifacts_request_kwargs(self.config, id))
        return _txn_artifacts(out)

    def problems(self, id: str) -> dict[str, Any]:
        out = self._request(**_txn_problems_request_kwargs(self.config, id))
        return _txn_problems(out)


@dataclass
class TxnAsyncHttpGateway:
    """Transactions gateway backed by Direct Access HTTP endpoints (async)."""

    config: "Config"
    base_url_provider: BaseUrlProvider
    http: AsyncHttpClient

    async def _request(
        self,
        *,
        operation: str,
        method: str,
        endpoint: str,
        path_params: dict[str, Any] | None = None,
        query_params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        allow_404: bool = False,
    ) -> Any:
        _status, content = await request_json_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=json_body,
            headers=_txn_default_headers(headers),
            allow_404=allow_404,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Transactions",
        )
        return content

    async def create(self, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        out = await self._request(
            **_txn_create_request_kwargs(self.config, reasoner_name=reasoner_name, payload=payload, timeout_mins=timeout_mins)
        )
        return _require_txn_id(out)

    async def get(self, id: str) -> JobRecord | None:
        out = await self._request(**_txn_get_request_kwargs(self.config, id))
        return _txn_job_record(out)

    async def events(self, id: str, continuation_token: str = "", *, stream_name: str = "progress") -> JobEvents:
        out = await self._request(**_txn_events_request_kwargs(self.config, id, continuation_token, stream_name))
        return _txn_events(out, txn_id=id, continuation_token=continuation_token, stream_name=stream_name)

    async def artifacts(self, id: str) -> dict[str, dict[str, Any]]:
        out = await self._request(**_txn_artifacts_request_kwargs(self.config, id))
        return _txn_artifacts(out)

    async def problems(self, id: str) -> dict[str, Any]:
        out = await self._request(**_txn_problems_request_kwargs(self.config, id))
        return _txn_problems(out)


# ---------------------------------------------------------------------------
# Router gateways (unified Jobs facade).
# ---------------------------------------------------------------------------


class _TxnSqlGateway(Protocol):
    def create(self, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str: ...
    def get(self, id: str) -> JobRecord | None: ...
    def list(
        self,
        *,
        job_id: str | None = None,
        state: str | None = None,
        name: str | None = None,
        created_by: str | None = None,
        only_active: bool = False,
        all_users: bool = False,
        limit: int | None = None,
    ) -> list[JobRecord]: ...
    def events(self, id: str, continuation_token: str = "", *, stream_name: str = "progress") -> JobEvents: ...
    def cancel(self, id: str, *, all_users: bool = False) -> None: ...
    def artifacts(self, id: str) -> dict[str, Any]: ...


class _TxnDaGatewaySync(Protocol):
    def create(self, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str: ...
    def get(self, id: str) -> JobRecord | None: ...
    def events(self, id: str, continuation_token: str = "", *, stream_name: str = "progress") -> JobEvents: ...
    def artifacts(self, id: str) -> dict[str, Any]: ...
    def problems(self, id: str) -> dict[str, Any]: ...


class TxnDaGatewayAsync(Protocol):
    async def create(self, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str: ...
    async def get(self, id: str) -> JobRecord | None: ...
    async def events(self, id: str, continuation_token: str = "", *, stream_name: str = "progress") -> JobEvents: ...
    async def artifacts(self, id: str) -> dict[str, Any]: ...
    async def problems(self, id: str) -> dict[str, Any]: ...


@dataclass
class LogicTransactionsGatewaySync:
    txn_sql: _TxnSqlGateway
    txn_http: _TxnDaGatewaySync | None = None

    def _require_logic(self, reasoner_type: str) -> None:
        if not _is_logic(reasoner_type):
            raise JobInvalidRequestError("Only supported for reasoner_type='LOGIC'.")

    def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        self._require_logic(reasoner_type)
        if self.txn_http is not None:
            return self.txn_http.get(job_id)
        return self.txn_sql.get(job_id)

    def create(self, reasoner_type: str, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        self._require_logic(reasoner_type)
        if self.txn_http is not None:
            return self.txn_http.create(reasoner_name, payload, timeout_mins=timeout_mins)
        return self.txn_sql.create(reasoner_name, payload, timeout_mins=timeout_mins)

    def list(self, reasoner_type: str, **kwargs: Any) -> list[JobRecord]:
        self._require_logic(reasoner_type)
        return self.txn_sql.list(**kwargs)

    def events(
        self, reasoner_type: str, job_id: str, continuation_token: str = "", *, stream_name: str = "progress"
    ) -> JobEvents:
        self._require_logic(reasoner_type)
        if self.txn_http is not None:
            return self.txn_http.events(job_id, continuation_token=continuation_token, stream_name=stream_name)
        return self.txn_sql.events(job_id, continuation_token=continuation_token, stream_name=stream_name)

    def cancel(self, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        self._require_logic(reasoner_type)
        return self.txn_sql.cancel(job_id, all_users=all_users)

    def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        self._require_logic(reasoner_type)
        if self.txn_http is None:
            return self.txn_sql.artifacts(job_id)
        return self.txn_http.artifacts(job_id)

    def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        self._require_logic(reasoner_type)
        if self.txn_http is None:
            return None
        return self.txn_http.problems(job_id)

    def close(self) -> None:
        return None


@dataclass
class LogicTransactionsGateway:
    txn_sql: _TxnSqlGateway
    txn_http: TxnDaGatewayAsync | None = None

    def _require_logic(self, reasoner_type: str) -> None:
        if not _is_logic(reasoner_type):
            raise JobInvalidRequestError("Only supported for reasoner_type='LOGIC'.")

    async def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        self._require_logic(reasoner_type)
        if self.txn_http is not None:
            return await self.txn_http.get(job_id)
        return await asyncio.to_thread(self.txn_sql.get, job_id)

    async def create(self, reasoner_type: str, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        self._require_logic(reasoner_type)
        if self.txn_http is not None:
            return await self.txn_http.create(reasoner_name, payload, timeout_mins=timeout_mins)
        return await asyncio.to_thread(self.txn_sql.create, reasoner_name, payload, timeout_mins=timeout_mins)

    async def list(self, reasoner_type: str, **kwargs: Any) -> list[JobRecord]:
        self._require_logic(reasoner_type)
        return await asyncio.to_thread(self.txn_sql.list, **kwargs)

    async def events(
        self, reasoner_type: str, job_id: str, continuation_token: str = "", *, stream_name: str = "progress"
    ) -> JobEvents:
        self._require_logic(reasoner_type)
        if self.txn_http is not None:
            return await self.txn_http.events(job_id, continuation_token=continuation_token, stream_name=stream_name)
        return await asyncio.to_thread(
            self.txn_sql.events, job_id, continuation_token, stream_name=stream_name
        )

    async def cancel(self, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        self._require_logic(reasoner_type)
        return await asyncio.to_thread(self.txn_sql.cancel, job_id, all_users=all_users)

    async def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        self._require_logic(reasoner_type)
        if self.txn_http is None:
            return await asyncio.to_thread(self.txn_sql.artifacts, job_id)
        return await self.txn_http.artifacts(job_id)

    async def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        self._require_logic(reasoner_type)
        if self.txn_http is None:
            return None
        return await self.txn_http.problems(job_id)

    async def aclose(self) -> None:
        return None


@dataclass
class RouterGatewaySync:
    jobs: JobsGatewaySync
    logic: JobsGatewaySync

    def _gw(self, reasoner_type: str) -> JobsGatewaySync:
        return self.logic if _is_logic(reasoner_type) else self.jobs

    def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        return self._gw(reasoner_type).get(reasoner_type, job_id)

    def create(self, reasoner_type: str, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        return self._gw(reasoner_type).create(reasoner_type, reasoner_name, payload, timeout_mins=timeout_mins)

    def list(self, reasoner_type: str, **kwargs: Any) -> list[JobRecord]:
        return self._gw(reasoner_type).list(reasoner_type, **kwargs)

    def events(
        self, reasoner_type: str, job_id: str, continuation_token: str = "", *, stream_name: str = "progress"
    ) -> JobEvents:
        return self._gw(reasoner_type).events(reasoner_type, job_id, continuation_token=continuation_token, stream_name=stream_name)

    def cancel(self, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        return self._gw(reasoner_type).cancel(reasoner_type, job_id, all_users=all_users)

    def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        return self._gw(reasoner_type).artifacts(reasoner_type, job_id)

    def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        return self._gw(reasoner_type).problems(reasoner_type, job_id)

    def close(self) -> None:
        return None


@dataclass
class RouterGateway:
    jobs: JobsGateway
    logic: JobsGateway

    def _gw(self, reasoner_type: str) -> JobsGateway:
        return self.logic if _is_logic(reasoner_type) else self.jobs

    async def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        return await self._gw(reasoner_type).get(reasoner_type, job_id)

    async def create(self, reasoner_type: str, reasoner_name: str, payload: dict[str, Any], *, timeout_mins: int | None = None) -> str:
        return await self._gw(reasoner_type).create(reasoner_type, reasoner_name, payload, timeout_mins=timeout_mins)

    async def list(self, reasoner_type: str, **kwargs: Any) -> list[JobRecord]:
        return await self._gw(reasoner_type).list(reasoner_type, **kwargs)

    async def events(
        self, reasoner_type: str, job_id: str, continuation_token: str = "", *, stream_name: str = "progress"
    ) -> JobEvents:
        return await self._gw(reasoner_type).events(reasoner_type, job_id, continuation_token=continuation_token, stream_name=stream_name)

    async def cancel(self, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        return await self._gw(reasoner_type).cancel(reasoner_type, job_id, all_users=all_users)

    async def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        return await self._gw(reasoner_type).artifacts(reasoner_type, job_id)

    async def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        return await self._gw(reasoner_type).problems(reasoner_type, job_id)

    async def aclose(self) -> None:
        return None


__all__ = [
    "LogicTransactionsGateway",
    "LogicTransactionsGatewaySync",
    "RouterGateway",
    "RouterGatewaySync",
    "TxnDaGatewayAsync",
    "TxnAsyncHttpGateway",
    "TxnHttpGateway",
    "TxnSqlGateway",
]

