from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any, TypeAlias

from relationalai.services._shared.da_request import BaseUrlProvider, request_async, request_sync

from .errors import JobNotReadyError, is_jobs_not_ready_message

if TYPE_CHECKING:
    from relationalai.client.transports.http import AsyncHttpClient, HttpClient
    import requests

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


def _raise_for_http_response(
    *,
    op: str,
    resp: HttpResponse,
    invalid_exc: type[Exception],
    server_exc: type[Exception],
    prefix: str,
) -> None:
    if resp.status_code == 200:
        return
    payload_dict: dict[str, Any] | None = None
    try:
        payload = resp.json()
        payload_dict = payload if isinstance(payload, dict) else None
        msg = payload.get("message") or payload.get("error") or json.dumps(payload)
    except Exception:
        msg = getattr(resp, "text", None) or f"HTTP {resp.status_code}"
    if resp.status_code == 400:
        raise invalid_exc(f"{prefix} request failed validation. {msg}")
    # Backend compatibility: some DA jobs endpoints report "not ready to accept
    # jobs" as HTTP 404 instead of a transient status like 409/503. Only remap
    # that specific structured payload shape; other 404s remain server/not-found
    # style failures.
    if int(resp.status_code) == 404 and _is_structured_jobs_not_ready(payload_dict):
        raise JobNotReadyError(operation=op, details=str(msg))
    raise server_exc(f"{prefix} request failed (status={resp.status_code}) op={op}. {msg}")


def _raise_for_status_content(
    *,
    op: str,
    status: int,
    content: Any,
    invalid_exc: type[Exception],
    server_exc: type[Exception],
    prefix: str,
) -> None:
    if int(status) == 200:
        return
    payload_dict = content if isinstance(content, dict) else None
    if isinstance(content, dict):
        msg = str(content.get("message") or content.get("error") or json.dumps(content))
    else:
        msg = str(content)
    if int(status) == 400:
        raise invalid_exc(f"{prefix} request failed validation. {msg}")
    # Keep sync/async content-based mapping aligned with the raw-response path
    # above for the DA backend's 404 "not ready" compatibility behavior.
    if int(status) == 404 and _is_structured_jobs_not_ready(payload_dict):
        raise JobNotReadyError(operation=op, details=str(msg))
    raise server_exc(f"{prefix} request failed (status={status}) op={op}. {msg}")


def _is_structured_jobs_not_ready(content: dict[str, Any] | None) -> bool:
    """Match structured DA payloads before they are wrapped into generic errors."""
    if not isinstance(content, dict):
        return False
    for key in ("message", "error"):
        if is_jobs_not_ready_message(content.get(key)):
            return True
    return False


def _allow_404_result(status: int, content: Any, *, op: str) -> tuple[int, Any]:
    # Jobs get() uses allow_404 to map true "not found" responses to None, but
    # some DA backends also encode transient "not ready to accept jobs" states as
    # structured 404 payloads. Keep those surfaced as JobNotReadyError instead of
    # collapsing them into "missing job".
    if int(status) == 404 and _is_structured_jobs_not_ready(content if isinstance(content, dict) else None):
        msg = (
            str(content.get("message") or content.get("error") or json.dumps(content))
            if isinstance(content, dict)
            else str(content)
        )
        raise JobNotReadyError(operation=op, details=msg)
    return int(status), None


def request_json_sync(
    *,
    http: "HttpClient",
    operation: str,
    method: str,
    base_url_provider: BaseUrlProvider,
    endpoint: str,
    path_params: dict[str, Any] | None = None,
    query_params: dict[str, Any] | None = None,
    payload: dict[str, Any] | None = None,
    headers: dict[str, str] | None = None,
    allow_404: bool = False,
    invalid_exc: type[Exception],
    server_exc: type[Exception],
    prefix: str,
) -> tuple[int, Any]:
    """Perform a sync Direct Access request and return `(status_code, json_content)`."""
    resp = request_sync(
        http=http,
        operation=operation,
        method=method,
        base_url_provider=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 resp.status_code == 404:
        try:
            content = resp.json()
        except Exception:
            content = None
        return _allow_404_result(int(resp.status_code), content, op=operation)
    _raise_for_http_response(op=operation, resp=resp, invalid_exc=invalid_exc, server_exc=server_exc, prefix=prefix)
    try:
        return int(resp.status_code), resp.json()
    except Exception:
        return int(resp.status_code), {}


async def request_json_async(
    *,
    http: "AsyncHttpClient",
    operation: str,
    method: str,
    base_url_provider: BaseUrlProvider,
    endpoint: str,
    path_params: dict[str, Any] | None = None,
    query_params: dict[str, Any] | None = None,
    payload: dict[str, Any] | None = None,
    headers: dict[str, str] | None = None,
    allow_404: bool = False,
    invalid_exc: type[Exception],
    server_exc: type[Exception],
    prefix: str,
) -> tuple[int, Any]:
    """Perform an async Direct Access request and return `(status, json_content)`."""
    status, content = await request_async(
        http=http,
        operation=operation,
        method=method,
        base_url_provider=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 _allow_404_result(int(status), content, op=operation)
    _raise_for_status_content(
        op=operation,
        status=int(status),
        content=content,
        invalid_exc=invalid_exc,
        server_exc=server_exc,
        prefix=prefix,
    )
    return int(status), content if content is not None else {}


__all__ = ["request_json_async", "request_json_sync"]
