# #region AsyncNetworkModule [C:5] [TYPE Module] [SEMANTICS auth, superset, token, async-client]
#
# @BRIEF Provides async Superset API client with shared auth-token cache to avoid per-request re-login.
# @LAYER Infrastructure
# @PRE  Config payloads contain a Superset base URL and authentication fields needed for login.
# @POST  Async network clients reuse cached auth tokens and expose stable async request/error translation flow.
# @SIDE_EFFECT  Performs upstream HTTP I/O and mutates process-local auth cache entries.
# @DATA_CONTRACT  Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions]
# @RELATION DEPENDS_ON -> [SupersetAuthCache]
# @INVARIANT  Async client reuses cached auth tokens per environment credentials and invalidates on 401.
import asyncio
from typing import Any

import httpx

from ..logger import belief_scope, logger as app_logger
from .network import (
    AuthenticationError,
    DashboardNotFoundError,
    NetworkError,
    PermissionDeniedError,
    SupersetAPIError,
    SupersetAuthCache,
)


# #region AsyncAPIClient [C:3] [TYPE Class]
# @BRIEF Async Superset API client backed by httpx.AsyncClient with shared auth cache.
# @RELATION DEPENDS_ON -> [SupersetAuthCache]
# @RELATION CALLS -> [SupersetAuthCache.get]
# @RELATION CALLS -> [SupersetAuthCache.set]
class AsyncAPIClient:
    DEFAULT_TIMEOUT = 30
    _auth_locks: dict[tuple[str, str, bool], asyncio.Lock] = {}
    # #region AsyncAPIClient.__init__ [TYPE Function] [C:3]
    # @PURPOSE: Initialize async API client for one environment.
    # @PRE  config contains base_url and auth payload.
    # @POST  Client is ready for async request/authentication flow.
    # @DATA_CONTRACT  Input[config: Dict[str, Any]] -> self._auth_cache_key[str]
    # @RELATION CALLS -> [AsyncAPIClient._normalize_base_url]
    # @RELATION DEPENDS_ON -> [SupersetAuthCache]
    def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):
        self.base_url: str = self._normalize_base_url(config.get("base_url", ""))
        self.api_base_url: str = f"{self.base_url}/api/v1"
        self.auth = config.get("auth")
        self.request_settings = {"verify_ssl": verify_ssl, "timeout": timeout}
        self._client = httpx.AsyncClient(
            verify=verify_ssl,
            timeout=httpx.Timeout(timeout),
            follow_redirects=True,
        )
        self._tokens: dict[str, str] = {}
        self._authenticated = False
        self._auth_cache_key = SupersetAuthCache.build_key(
            self.base_url,
            self.auth,
            verify_ssl,
        )
    # #endregion AsyncAPIClient.__init__
    # #region AsyncAPIClient._normalize_base_url [TYPE Function] [C:1]
    # @PURPOSE: Normalize base URL for Superset API root construction.
    # @POST  Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.
    def _normalize_base_url(self, raw_url: str) -> str:
        normalized = str(raw_url or "").strip().rstrip("/")
        if normalized.lower().endswith("/api/v1"):
            normalized = normalized[:-len("/api/v1")]
        return normalized.rstrip("/")
    # #endregion AsyncAPIClient._normalize_base_url
    # #region AsyncAPIClient._build_api_url [TYPE Function] [C:1]
    # @PURPOSE: Build full API URL from relative Superset endpoint.
    # @POST  Returns absolute URL for upstream request.
    def _build_api_url(self, endpoint: str) -> str:
        normalized_endpoint = str(endpoint or "").strip()
        if normalized_endpoint.startswith("http://") or normalized_endpoint.startswith("https://"):
            return normalized_endpoint
        if not normalized_endpoint.startswith("/"):
            normalized_endpoint = f"/{normalized_endpoint}"
        if normalized_endpoint.startswith("/api/v1/") or normalized_endpoint == "/api/v1":
            return f"{self.base_url}{normalized_endpoint}"
        return f"{self.api_base_url}{normalized_endpoint}"
    # #endregion AsyncAPIClient._build_api_url
    # #region AsyncAPIClient._get_auth_lock [TYPE Function] [C:1]
    # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.
    # @POST  Returns stable asyncio.Lock instance.
    @classmethod
    def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:
        existing_lock = cls._auth_locks.get(cache_key)
        if existing_lock is not None:
            return existing_lock
        created_lock = asyncio.Lock()
        cls._auth_locks[cache_key] = created_lock
        return created_lock
    # #endregion AsyncAPIClient._get_auth_lock
    # #region AsyncAPIClient.authenticate [TYPE Function] [C:3]
    # @PURPOSE: Authenticate against Superset and cache access/csrf tokens.
    # @POST  Client tokens are populated and reusable across requests.
    # @SIDE_EFFECT  Performs network requests to Superset authentication endpoints.
    # @DATA_CONTRACT  None -> Output[Dict[str, str]]
    # @RELATION CALLS -> [SupersetAuthCache.get]
    # @RELATION CALLS -> [SupersetAuthCache.set]
    # @RELATION CALLS -> [AsyncAPIClient._get_auth_lock]
    async def authenticate(self) -> dict[str, str]:
        cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
        if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
            self._tokens = cached_tokens
            self._authenticated = True
            app_logger.info("[async_authenticate][CacheHit] Reusing cached Superset auth tokens for %s", self.base_url)
            return self._tokens
        auth_lock = self._get_auth_lock(self._auth_cache_key)
        async with auth_lock:
            cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
            if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
                self._tokens = cached_tokens
                self._authenticated = True
                app_logger.info("[async_authenticate][CacheHitAfterWait] Reusing cached Superset auth tokens for %s", self.base_url)
                return self._tokens
            with belief_scope("AsyncAPIClient.authenticate"):
                app_logger.info("[async_authenticate][Enter] Authenticating to %s", self.base_url)
                try:
                    login_url = f"{self.api_base_url}/security/login"
                    response = await self._client.post(login_url, json=self.auth)
                    response.raise_for_status()
                    access_token = response.json()["access_token"]
                    csrf_url = f"{self.api_base_url}/security/csrf_token/"
                    csrf_response = await self._client.get(
                        csrf_url,
                        headers={"Authorization": f"Bearer {access_token}"},
                    )
                    csrf_response.raise_for_status()
                    self._tokens = {
                        "access_token": access_token,
                        "csrf_token": csrf_response.json()["result"],
                    }
                    self._authenticated = True
                    SupersetAuthCache.set(self._auth_cache_key, self._tokens)
                    app_logger.reflect("Authenticated successfully.", extra={"src": "async_authenticate"})
                    return self._tokens
                except httpx.HTTPStatusError as exc:
                    SupersetAuthCache.invalidate(self._auth_cache_key)
                    status_code = exc.response.status_code if exc.response is not None else None
                    if status_code in [502, 503, 504]:
                        raise NetworkError(
                            f"Environment unavailable during authentication (Status {status_code})",
                            status_code=status_code,
                        ) from exc
                    raise AuthenticationError(f"Authentication failed: {exc}") from exc
                except (httpx.HTTPError, KeyError) as exc:
                    SupersetAuthCache.invalidate(self._auth_cache_key)
                    raise NetworkError(f"Network or parsing error during authentication: {exc}") from exc
    # #endregion AsyncAPIClient.authenticate
    # #region AsyncAPIClient.get_headers [TYPE Function] [C:3]
    # @PURPOSE: Return authenticated Superset headers for async requests.
    # @POST  Headers include Authorization and CSRF tokens.
    # @RELATION CALLS -> [AsyncAPIClient.authenticate]
    async def get_headers(self) -> dict[str, str]:
        if not self._authenticated:
            await self.authenticate()
        return {
            "Authorization": f"Bearer {self._tokens['access_token']}",
            "X-CSRFToken": self._tokens.get("csrf_token", ""),
            "Referer": self.base_url,
            "Content-Type": "application/json",
        }
    # #endregion AsyncAPIClient.get_headers
    # #region AsyncAPIClient.request [TYPE Function] [C:3]
    # @PURPOSE: Perform one authenticated async Superset API request.
    # @POST  Returns JSON payload or raw httpx.Response when raw_response=true.
    # @SIDE_EFFECT  Performs network I/O.
    # @RELATION CALLS -> [AsyncAPIClient.get_headers]
    # @RELATION CALLS -> [EXT:method:AsyncAPIClient._handle_http_error]
    # @RELATION CALLS -> [AsyncAPIClient._handle_network_error]
    # @RELATION CALLS -> [AsyncAPIClient._is_dashboard_endpoint]
    # @RELATION DEPENDS_ON -> [DashboardNotFoundError]
    # @RELATION DEPENDS_ON -> [SupersetAPIError]
    # @RELATION DEPENDS_ON -> [PermissionDeniedError]
    # @RELATION DEPENDS_ON -> [AuthenticationError]
    # @RELATION DEPENDS_ON -> [NetworkError]
    def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:
        with belief_scope("AsyncAPIClient._handle_http_error"):
            status_code = exc.response.status_code
        if status_code in [502, 503, 504]:
            raise NetworkError(f"Environment unavailable (Status {status_code})", status_code=status_code) from exc
        if status_code == 404:
            if self._is_dashboard_endpoint(endpoint):
                raise DashboardNotFoundError(endpoint) from exc
            raise SupersetAPIError(
                f"API resource not found at endpoint '{endpoint}'",
                status_code=status_code,
                endpoint=endpoint,
                subtype="not_found",
            ) from exc
        if status_code == 403:
            raise PermissionDeniedError() from exc
        if status_code == 401:
            raise AuthenticationError() from exc
        raise SupersetAPIError(f"API Error {status_code}: {exc.response.text}") from exc
    # #endregion AsyncAPIClient._handle_http_error
    # #region AsyncAPIClient._is_dashboard_endpoint [TYPE Function] [C:2]
    # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.
    # @POST  Returns true only for dashboard-specific endpoints.
    def _is_dashboard_endpoint(self, endpoint: str) -> bool:
        normalized_endpoint = str(endpoint or "").strip().lower()
        if not normalized_endpoint:
            return False
        if normalized_endpoint.startswith("http://") or normalized_endpoint.startswith("https://"):
            try:
                normalized_endpoint = "/" + normalized_endpoint.split("/api/v1", 1)[1].lstrip("/")
            except IndexError:
                return False
        if normalized_endpoint.startswith("/api/v1/"):
            normalized_endpoint = normalized_endpoint[len("/api/v1"):]
        return normalized_endpoint.startswith("/dashboard/") or normalized_endpoint == "/dashboard"
    # #endregion AsyncAPIClient._is_dashboard_endpoint
    # #region AsyncAPIClient._handle_network_error [TYPE Function] [C:3]
    # @PURPOSE: Translate generic httpx errors into NetworkError.
    # @POST  Raises NetworkError with URL context.
    # @DATA_CONTRACT  Input[httpx.HTTPError] -> NetworkError
    # @RELATION DEPENDS_ON -> [NetworkError]
    def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:
        with belief_scope("AsyncAPIClient._handle_network_error"):
            if isinstance(exc, httpx.TimeoutException):
                message = "Request timeout"
            elif isinstance(exc, httpx.ConnectError):
                message = "Connection error"
            else:
                message = f"Unknown network error: {exc}"
            raise NetworkError(message, url=url) from exc
    # #endregion AsyncAPIClient._handle_network_error
    # #region AsyncAPIClient.aclose [TYPE Function] [C:3]
    # @PURPOSE: Close underlying httpx client.
    # @POST  Client resources are released.
    # @SIDE_EFFECT  Closes network connections.
    # @RELATION DEPENDS_ON -> [AsyncAPIClient.__init__]
    async def aclose(self) -> None:
        await self._client.aclose()
    # #endregion AsyncAPIClient.aclose
# #endregion AsyncAPIClient
# #endregion AsyncNetworkModule
