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

from __future__ import annotations

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

from relationalai.client.transports.http import AsyncHttpClient, HttpClient
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

from .da_http import request_json_async, request_json_sync
from .constants import DEFAULT_JOBS_SCHEMA, JOB_ACTIVE_STATES, JOBS_DA_BASE_PATH
from .errors import JobInvalidRequestError, JobServerError
from .models import JobEvents, JobRecord, job_events_from_value, job_from_row

if TYPE_CHECKING:
    from relationalai.client.transports.sql import SqlClient
    from relationalai.config.config import Config


class JobsGatewaySync(Protocol):
    def get(self, reasoner_type: str, job_id: str) -> JobRecord | None: ...

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

    def list(
        self,
        reasoner_type: str,
        *,
        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,
        reasoner_type: str,
        job_id: str,
        continuation_token: str = "",
        *,
        stream_name: str = "progress",
    ) -> JobEvents: ...

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

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

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

    def close(self) -> None: ...


class JobsGateway(Protocol):
    async def get(self, reasoner_type: str, job_id: str) -> JobRecord | None: ...

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

    async def list(
        self,
        reasoner_type: str,
        *,
        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]: ...

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

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

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

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

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


@dataclass
class SqlGateway:
    """Jobs gateway implemented via Snowflake SQL stored procedure(s)."""

    executor: "SqlClient"
    app: str
    schema: str = DEFAULT_JOBS_SCHEMA

    def _execute(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": self.schema,
                "procedure": "GET_JOB",
                "params": 0 if not params else len(params),
            },
        )

    def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        """Get a job by `(type, id)`, returning `None` if not found."""
        stmt = f"call {self.app}.api.GET_JOB(?, ?);"
        rows = self._execute(stmt, [reasoner_type, job_id], operation="get")
        if not rows:
            return None
        return job_from_row(rows[0])

    def create(
        self,
        reasoner_type: str,
        reasoner_name: str,
        payload: dict[str, Any],
        *,
        timeout_mins: int | None = None,
    ) -> str:
        """Create a job asynchronously and return the job id."""
        payload_json = json.dumps(payload)

        if timeout_mins is None:
            stmt = f"call {self.app}.api.EXEC_JOB_ASYNC(?, ?, ?);"
            rows = self._execute(stmt, [reasoner_type, reasoner_name, payload_json], operation="create")
        else:
            stmt = f"call {self.app}.api.EXEC_JOB_ASYNC(?, ?, ?, null, ?);"
            rows = self._execute(
                stmt, [reasoner_type, reasoner_name, payload_json, int(timeout_mins)], operation="create"
            )

        if not rows:
            raise RuntimeError("Job creation returned no results (expected an ID row).")
        job_id = row_get(rows[0], "ID", "id", "JOB_ID", "job_id")
        if not isinstance(job_id, str):
            job_id = str(job_id) if job_id is not None else ""
        if not job_id:
            raise RuntimeError("Job creation did not return an ID.")
        return job_id

    def list(
        self,
        reasoner_type: str,
        *,
        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]:
        """List jobs for a given reasoner type (SQL backend)."""
        where: list[str] = ["REASONER_TYPE = ?"]
        params: list[Any] = [reasoner_type]

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

        if state is not None:
            where.append("STATE = ?")
            params.append(state)
        elif only_active:
            states = ", ".join(f"'{s}'" for s in sorted(JOB_ACTIVE_STATES))
            where.append(f"STATE in ({states})")

        if name:
            where.append("LOWER(REASONER_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 = " AND ".join(where)
        limit_clause = ""
        if limit is not None:
            limit_int = int(limit)
            if limit_int <= 0:
                return []
            limit_clause = f" LIMIT {limit_int}"

        stmt = f"""
            SELECT
                ID, REASONER_TYPE, REASONER_NAME, STATE, CREATED_BY, CREATED_ON, FINISHED_AT, DURATION,
                PAYLOAD, TIMEOUT_MS, ABORT_REASON
            FROM {self.app}.{self.schema}.JOBS
            WHERE {where_clause}
            ORDER BY CREATED_ON DESC{limit_clause};
        """
        rows = self._execute(stmt, params, operation="list")
        return [job_from_row(r) for r in rows] if rows else []

    def events(
        self,
        reasoner_type: str,
        job_id: str,
        continuation_token: str = "",
        *,
        stream_name: str = "progress",
    ) -> JobEvents:
        """Fetch job events (e.g. progress stream)."""
        # SQL path uses a stored function without a stream selector.
        if stream_name and stream_name != "progress":
            raise JobInvalidRequestError("SQL backend for jobs only supports stream_name='progress'.")
        stmt = f"select {self.app}.api.GET_JOB_EVENTS(?, ?, ?);"
        rows = self._execute(stmt, [reasoner_type, job_id, continuation_token], 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, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        """Cancel a job."""
        if all_users:
            raise JobInvalidRequestError("Jobs cancel does not support all_users=True.")
        stmt = f"call {self.app}.api.CANCEL_JOB(?, ?);"
        self._execute(stmt, [reasoner_type, job_id], operation="cancel")
        return None

    def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        # Non-LOGIC jobs do not have transaction artifacts.
        _ = reasoner_type, job_id
        return None

    def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        # Non-LOGIC jobs do not have transaction problems.
        _ = reasoner_type, job_id
        return None

    def close(self) -> None:
        # Gateways do not own shared transports; root client closes them.
        return None


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

    config: "Config"
    base_url_provider: BaseUrlProvider
    http: HttpClient

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

    def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        op = "get"
        status, content = request_json_sync(
            http=self.http,
            operation=op,
            method="GET",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=f"{JOBS_DA_BASE_PATH}/{{job_type}}/{{job_id}}",
            path_params={"job_type": reasoner_type.upper(), "job_id": job_id},
            allow_404=True,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        if status == 404:
            return None
        job = content.get("job") if isinstance(content, dict) else None
        if not isinstance(job, dict):
            return None
        # Ensure type is present (some list/get shapes omit it).
        job.setdefault("type", reasoner_type.upper())
        return job_from_row(job)

    def create(
        self,
        reasoner_type: str,
        reasoner_name: str,
        payload: dict[str, Any],
        *,
        timeout_mins: int | None = None,
    ) -> str:
        op = "create"
        req: dict[str, Any] = {
            "job_type": reasoner_type.upper(),
            # ERP uses "worker_name" in some places; allow backend to map as needed.
            "worker_name": reasoner_name,
            "payload": json.dumps(payload),
        }
        if timeout_mins is not None:
            req["timeout_mins"] = int(timeout_mins)

        _status, content = request_json_sync(
            http=self.http,
            operation=op,
            method="POST",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=JOBS_DA_BASE_PATH,
            payload=req,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        job_id = content.get("id") if isinstance(content, dict) else None
        if not isinstance(job_id, str) or not job_id:
            raise JobServerError("Jobs create did not return an id.")
        return job_id

    def list(
        self,
        reasoner_type: str,
        *,
        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]:
        op = "list"
        if limit is not None and int(limit) <= 0:
            return []

        if job_id:
            j = self.get(reasoner_type, job_id)
            if j is None:
                return []
            if state is not None and getattr(j, "state", None) and str(getattr(j, "state", "")).upper() != state.upper():
                return []
            if state is None and only_active and str(getattr(j, "state", "")).upper() not in JOB_ACTIVE_STATES:
                return []
            if name is not None and str(getattr(j, "reasoner_name", "")).lower() != str(name).lower():
                return []
            if not all_users and created_by is not None and str(getattr(j, "created_by", "")).lower() != str(created_by).lower():
                return []
            return [j]

        _status, content = request_json_sync(
            http=self.http,
            operation=op,
            method="GET",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=JOBS_DA_BASE_PATH,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        jobs = content.get("jobs", []) if isinstance(content, dict) else []
        return _filter_da_jobs(
            reasoner_type=reasoner_type,
            jobs=jobs,
            state=state,
            name=name,
            created_by=created_by,
            only_active=only_active,
            all_users=all_users,
            limit=limit,
        )

    def events(
        self,
        reasoner_type: str,
        job_id: str,
        continuation_token: str = "",
        *,
        stream_name: str = "progress",
    ) -> JobEvents:
        op = "events"
        _status, content = request_json_sync(
            http=self.http,
            operation=op,
            method="GET",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=f"{JOBS_DA_BASE_PATH}/{{job_type}}/{{job_id}}/events/{{stream_name}}",
            path_params={
                "job_type": reasoner_type.upper(),
                "job_id": job_id,
                "stream_name": stream_name or "progress",
            },
            query_params={"continuation_token": continuation_token} if continuation_token else None,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        return job_events_from_value(content or {})

    def cancel(self, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        op = "cancel"
        if all_users:
            raise JobInvalidRequestError("Jobs cancel does not support all_users=True.")
        _status, _content = request_json_sync(
            http=self.http,
            operation=op,
            method="POST",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=f"{JOBS_DA_BASE_PATH}/{{job_type}}/{{job_id}}/cancel",
            path_params={"job_type": reasoner_type.upper(), "job_id": job_id},
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        return None

    def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        # Non-LOGIC jobs do not have transaction artifacts.
        _ = reasoner_type, job_id
        return None

    def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        # Non-LOGIC jobs do not have transaction problems.
        _ = reasoner_type, job_id
        return None

    def close(self) -> None:
        return None


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

    config: "Config"
    base_url_provider: BaseUrlProvider
    http: AsyncHttpClient

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

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

    async def get(self, reasoner_type: str, job_id: str) -> JobRecord | None:
        op = "get"
        status, content = await request_json_async(
            http=self.http,
            operation=op,
            method="GET",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=f"{JOBS_DA_BASE_PATH}/{{job_type}}/{{job_id}}",
            path_params={"job_type": reasoner_type.upper(), "job_id": job_id},
            allow_404=True,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        if status == 404:
            return None
        job = content.get("job") if isinstance(content, dict) else None
        if not isinstance(job, dict):
            return None
        job.setdefault("type", reasoner_type.upper())
        return job_from_row(job)

    async def create(
        self,
        reasoner_type: str,
        reasoner_name: str,
        payload: dict[str, Any],
        *,
        timeout_mins: int | None = None,
    ) -> str:
        op = "create"
        req: dict[str, Any] = {
            "job_type": reasoner_type.upper(),
            "worker_name": reasoner_name,
            "payload": json.dumps(payload),
        }
        if timeout_mins is not None:
            req["timeout_mins"] = int(timeout_mins)

        _status, content = await request_json_async(
            http=self.http,
            operation=op,
            method="POST",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=JOBS_DA_BASE_PATH,
            payload=req,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        job_id = content.get("id") if isinstance(content, dict) else None
        if not isinstance(job_id, str) or not job_id:
            raise JobServerError("Jobs create did not return an id.")
        return job_id

    async def list(
        self,
        reasoner_type: str,
        *,
        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]:
        op = "list"
        if limit is not None and int(limit) <= 0:
            return []

        if job_id:
            j = await self.get(reasoner_type, job_id)
            if j is None:
                return []
            if state is not None and getattr(j, "state", None) and str(getattr(j, "state", "")).upper() != state.upper():
                return []
            if state is None and only_active and str(getattr(j, "state", "")).upper() not in JOB_ACTIVE_STATES:
                return []
            if name is not None and str(getattr(j, "reasoner_name", "")).lower() != str(name).lower():
                return []
            if not all_users and created_by is not None and str(getattr(j, "created_by", "")).lower() != str(created_by).lower():
                return []
            return [j]

        _status, content = await request_json_async(
            http=self.http,
            operation=op,
            method="GET",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=JOBS_DA_BASE_PATH,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        jobs = content.get("jobs", []) if isinstance(content, dict) else []
        return _filter_da_jobs(
            reasoner_type=reasoner_type,
            jobs=jobs,
            state=state,
            name=name,
            created_by=created_by,
            only_active=only_active,
            all_users=all_users,
            limit=limit,
        )

    async def events(
        self,
        reasoner_type: str,
        job_id: str,
        continuation_token: str = "",
        *,
        stream_name: str = "progress",
    ) -> JobEvents:
        op = "events"
        _status, content = await request_json_async(
            http=self.http,
            operation=op,
            method="GET",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=f"{JOBS_DA_BASE_PATH}/{{job_type}}/{{job_id}}/events/{{stream_name}}",
            path_params={
                "job_type": reasoner_type.upper(),
                "job_id": job_id,
                "stream_name": stream_name or "progress",
            },
            query_params={"continuation_token": continuation_token} if continuation_token else None,
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        return job_events_from_value(content or {})

    async def cancel(self, reasoner_type: str, job_id: str, *, all_users: bool = False) -> None:
        op = "cancel"
        if all_users:
            raise JobInvalidRequestError("Jobs cancel does not support all_users=True.")
        _status, _content = await request_json_async(
            http=self.http,
            operation=op,
            method="POST",
            base_url_provider=self.base_url_provider,
            headers=self._headers(),
            endpoint=f"{JOBS_DA_BASE_PATH}/{{job_type}}/{{job_id}}/cancel",
            path_params={"job_type": reasoner_type.upper(), "job_id": job_id},
            invalid_exc=JobInvalidRequestError,
            server_exc=JobServerError,
            prefix="Jobs",
        )
        return None

    async def artifacts(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        # Non-LOGIC jobs do not have transaction artifacts.
        _ = reasoner_type, job_id
        return None

    async def problems(self, reasoner_type: str, job_id: str) -> dict[str, Any] | None:
        # Non-LOGIC jobs do not have transaction problems.
        _ = reasoner_type, job_id
        return None
def _filter_da_jobs(
    *,
    reasoner_type: str,
    jobs: Any,
    state: str | None,
    name: str | None,
    created_by: str | None,
    only_active: bool,
    all_users: bool,
    limit: int | None,
) -> list[JobRecord]:
    """Filter a Direct Access `GET /jobs` payload to match JobsGateway.list semantics."""
    rt = str(reasoner_type).upper()
    desired_state = state.upper() if isinstance(state, str) and state else None
    desired_rn = str(name).lower() if name is not None else None
    desired_cb = str(created_by).lower() if created_by is not None else None

    xs = jobs if isinstance(jobs, list) else []
    out: list[JobRecord] = []
    for j in xs:
        if not isinstance(j, dict):
            continue

        # Some DA payloads may not include type; enforce caller-provided type.
        j.setdefault("type", rt)

        if str(j.get("type", "")).upper() != rt:
            continue

        st = str(j.get("state", "")).upper()
        if desired_state is not None and st != desired_state:
            continue
        if desired_state is None and only_active and st not in JOB_ACTIVE_STATES:
            continue

        if desired_rn is not None:
            rn = j.get("engine_name") or j.get("worker_name") or ""
            if str(rn).lower() != desired_rn:
                continue

        if not all_users and desired_cb is not None:
            cb = j.get("created_by") or ""
            if str(cb).lower() != desired_cb:
                continue

        out.append(job_from_row(j))

    if limit is not None and int(limit) > 0:
        return out[: int(limit)]
    return out



__all__ = ["SqlGateway", "HttpGateway", "AsyncHttpGateway"]

