from __future__ import annotations

from typing import Any

from .._compat import optional_import
from .base import ErrorInfo, default_message

RequestsExceptions = optional_import("requests", "exceptions")
HTTPX = optional_import("httpx")
HTTPXAio = optional_import("aiohttp")
Urllib3Exceptions = optional_import("urllib3", "exceptions")
PyJWTExceptions = optional_import("jwt", "exceptions")
BotoCoreExceptions = optional_import("botocore.exceptions")
GRPC = optional_import("grpc")
GRPCAio = optional_import("grpc.aio")
ElasticExceptions = optional_import("elasticsearch")
ElasticTransportExceptions = optional_import("elastic_transport")


def _class(name: str, module: Any) -> type[Any] | None:
    return getattr(module, name, None) if module else None


def _response_status(response: Any, default: int = 502) -> int:
    return int(getattr(response, "status_code", getattr(response, "status", default)) or default)


def _http_error_info(status_code: int, *, code_4xx: str = "upstream_bad_response") -> tuple[int, str]:
    if status_code == 404:
        return 404, "upstream_not_found"
    if status_code == 409:
        return 409, "upstream_conflict"
    if status_code == 429:
        return 429, "upstream_rate_limited"
    if status_code in {401, 403}:
        return status_code, "upstream_forbidden" if status_code == 403 else "upstream_unauthenticated"
    if 400 <= status_code < 500:
        return status_code, code_4xx
    if status_code >= 500:
        return 502, "upstream_bad_response"
    return 502, "upstream_bad_response"


def _grpc_status_info(status_code: Any) -> tuple[int, str]:
    name = getattr(status_code, "name", str(status_code))
    mapping = {
        "DEADLINE_EXCEEDED": (504, "upstream_timeout"),
        "UNAVAILABLE": (503, "upstream_unavailable"),
        "RESOURCE_EXHAUSTED": (429, "upstream_rate_limited"),
        "NOT_FOUND": (404, "upstream_not_found"),
        "ALREADY_EXISTS": (409, "upstream_conflict"),
        "PERMISSION_DENIED": (403, "upstream_forbidden"),
        "UNAUTHENTICATED": (401, "upstream_unauthenticated"),
        "INVALID_ARGUMENT": (422, "upstream_invalid_request"),
        "FAILED_PRECONDITION": (422, "upstream_invalid_request"),
        "OUT_OF_RANGE": (422, "upstream_invalid_request"),
        "CANCELLED": (499, "upstream_cancelled"),
    }
    return mapping.get(name, (502, "upstream_bad_response"))


def _details_with_message(message: str, **extra: Any) -> list[dict[str, Any]] | None:
    details = {"message": message}
    details.update({key: value for key, value in extra.items() if value is not None})
    return [details]


def _upstream_retryable(code: str) -> bool | None:
    if code in ("upstream_timeout", "upstream_unavailable", "cloud_timeout", "cloud_unavailable", "auth_provider_unavailable"):
        return True
    if code in ("upstream_bad_response", "upstream_ssl_error", "upstream_proxy_error", "invalid_upstream_url",
                "upstream_unauthenticated", "upstream_forbidden", "invalid_token", "invalid_token_claim",
                "token_expired", "token_not_yet_valid", "invalid_cloud_params", "cloud_credentials_unavailable"):
        return False
    return None


def describe_http_auth_cloud_exception(exc: Exception) -> ErrorInfo | None:
    if RequestsExceptions:
        timeout_types = tuple(filter(None, (_class("Timeout", RequestsExceptions), _class("ConnectTimeout", RequestsExceptions), _class("ReadTimeout", RequestsExceptions))))
        if timeout_types and isinstance(exc, timeout_types):
            return ErrorInfo(504, "upstream_timeout", default_message(504), error_type=exc.__class__.__name__, retryable=True)

        connection_types = tuple(filter(None, (_class("ConnectionError", RequestsExceptions),)))
        if connection_types and isinstance(exc, connection_types):
            return ErrorInfo(503, "upstream_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)

        ssl_types = tuple(filter(None, (_class("SSLError", RequestsExceptions),)))
        if ssl_types and isinstance(exc, ssl_types):
            return ErrorInfo(502, "upstream_ssl_error", "A secure upstream connection failed.", error_type=exc.__class__.__name__, retryable=False)

        proxy_types = tuple(filter(None, (_class("ProxyError", RequestsExceptions),)))
        if proxy_types and isinstance(exc, proxy_types):
            return ErrorInfo(502, "upstream_proxy_error", "The upstream proxy request failed.", error_type=exc.__class__.__name__, retryable=False)

        invalid_url_types = tuple(
            filter(
                None,
                (
                    _class("URLRequired", RequestsExceptions),
                    _class("MissingSchema", RequestsExceptions),
                    _class("InvalidSchema", RequestsExceptions),
                    _class("InvalidURL", RequestsExceptions),
                    _class("InvalidProxyURL", RequestsExceptions),
                ),
            )
        )
        if invalid_url_types and isinstance(exc, invalid_url_types):
            return ErrorInfo(400, "invalid_upstream_url", "The upstream URL is invalid.", error_type=exc.__class__.__name__, retryable=False)

        http_error = _class("HTTPError", RequestsExceptions)
        if http_error and isinstance(exc, http_error):
            status_code, code = _http_error_info(_response_status(getattr(exc, "response", None)))
            return ErrorInfo(status_code, code, default_message(status_code), error_type=exc.__class__.__name__, retryable=_upstream_retryable(code))

        bad_response_types = tuple(filter(None, (_class("TooManyRedirects", RequestsExceptions), _class("ChunkedEncodingError", RequestsExceptions), _class("ContentDecodingError", RequestsExceptions), _class("JSONDecodeError", RequestsExceptions))))
        if bad_response_types and isinstance(exc, bad_response_types):
            return ErrorInfo(502, "upstream_bad_response", "The upstream service returned an invalid response.", error_type=exc.__class__.__name__, retryable=False)

    if HTTPX:
        timeout_types = tuple(filter(None, (getattr(HTTPX, "TimeoutException", None), getattr(HTTPX, "ConnectTimeout", None), getattr(HTTPX, "ReadTimeout", None), getattr(HTTPX, "WriteTimeout", None), getattr(HTTPX, "PoolTimeout", None))))
        if timeout_types and isinstance(exc, timeout_types):
            return ErrorInfo(504, "upstream_timeout", default_message(504), error_type=exc.__class__.__name__, retryable=True)

        unavailable_types = tuple(filter(None, (getattr(HTTPX, "ConnectError", None), getattr(HTTPX, "ReadError", None), getattr(HTTPX, "WriteError", None), getattr(HTTPX, "CloseError", None), getattr(HTTPX, "ProxyError", None))))
        if unavailable_types and isinstance(exc, unavailable_types):
            return ErrorInfo(503, "upstream_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)

        if isinstance(exc, tuple(filter(None, (getattr(HTTPX, "RemoteProtocolError", None), getattr(HTTPX, "DecodingError", None), getattr(HTTPX, "TooManyRedirects", None))))):
            return ErrorInfo(502, "upstream_bad_response", "The upstream service returned an invalid response.", error_type=exc.__class__.__name__, retryable=False)

        if isinstance(exc, tuple(filter(None, (getattr(HTTPX, "LocalProtocolError", None), getattr(HTTPX, "InvalidURL", None), getattr(HTTPX, "UnsupportedProtocol", None))))):
            return ErrorInfo(400, "invalid_upstream_url", "The upstream URL is invalid.", error_type=exc.__class__.__name__, retryable=False)

        http_status_error = getattr(HTTPX, "HTTPStatusError", None)
        if http_status_error and isinstance(exc, http_status_error):
            status_code, code = _http_error_info(_response_status(getattr(exc, "response", None)))
            return ErrorInfo(status_code, code, default_message(status_code), error_type=exc.__class__.__name__, retryable=_upstream_retryable(code))

    if HTTPXAio:
        timeout_types = tuple(filter(None, (getattr(HTTPXAio, "ServerTimeoutError", None),)))
        if timeout_types and isinstance(exc, timeout_types):
            return ErrorInfo(504, "upstream_timeout", default_message(504), error_type=exc.__class__.__name__, retryable=True)

        unavailable_types = tuple(filter(None, (getattr(HTTPXAio, "ClientConnectionError", None), getattr(HTTPXAio, "ClientConnectorError", None), getattr(HTTPXAio, "ServerConnectionError", None), getattr(HTTPXAio, "ServerDisconnectedError", None))))
        if unavailable_types and isinstance(exc, unavailable_types):
            return ErrorInfo(503, "upstream_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)

        ssl_types = tuple(filter(None, (getattr(HTTPXAio, "ClientSSLError", None), getattr(HTTPXAio, "ClientConnectorSSLError", None), getattr(HTTPXAio, "ClientConnectorCertificateError", None), getattr(HTTPXAio, "ServerFingerprintMismatch", None))))
        if ssl_types and isinstance(exc, ssl_types):
            return ErrorInfo(502, "upstream_ssl_error", "A secure upstream connection failed.", error_type=exc.__class__.__name__, retryable=False)

        bad_response_types = tuple(filter(None, (getattr(HTTPXAio, "ClientPayloadError", None), getattr(HTTPXAio, "TooManyRedirects", None))))
        if bad_response_types and isinstance(exc, bad_response_types):
            return ErrorInfo(502, "upstream_bad_response", "The upstream service returned an invalid response.", error_type=exc.__class__.__name__, retryable=False)

        invalid_url_type = getattr(HTTPXAio, "InvalidURL", None)
        if invalid_url_type and isinstance(exc, invalid_url_type):
            return ErrorInfo(400, "invalid_upstream_url", "The upstream URL is invalid.", error_type=exc.__class__.__name__, retryable=False)

        response_error = getattr(HTTPXAio, "ClientResponseError", None)
        if response_error and isinstance(exc, response_error):
            status_code, code = _http_error_info(_response_status(exc))
            return ErrorInfo(status_code, code, default_message(status_code), error_type=exc.__class__.__name__, retryable=_upstream_retryable(code))

    if Urllib3Exceptions:
        timeout_types = tuple(filter(None, (_class("TimeoutError", Urllib3Exceptions), _class("ConnectTimeoutError", Urllib3Exceptions), _class("ReadTimeoutError", Urllib3Exceptions))))
        if timeout_types and isinstance(exc, timeout_types):
            return ErrorInfo(504, "upstream_timeout", default_message(504), error_type=exc.__class__.__name__, retryable=True)

        connection_types = tuple(filter(None, (_class("ConnectionError", Urllib3Exceptions), _class("NewConnectionError", Urllib3Exceptions), _class("MaxRetryError", Urllib3Exceptions))))
        if connection_types and isinstance(exc, connection_types):
            return ErrorInfo(503, "upstream_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)

        ssl_types = tuple(filter(None, (_class("SSLError", Urllib3Exceptions),)))
        if ssl_types and isinstance(exc, ssl_types):
            return ErrorInfo(502, "upstream_ssl_error", "A secure upstream connection failed.", error_type=exc.__class__.__name__, retryable=False)

        url_types = tuple(filter(None, (_class("LocationValueError", Urllib3Exceptions), _class("LocationParseError", Urllib3Exceptions))))
        if url_types and isinstance(exc, url_types):
            return ErrorInfo(400, "invalid_upstream_url", "The upstream URL is invalid.", error_type=exc.__class__.__name__, retryable=False)

        base_error = _class("HTTPError", Urllib3Exceptions)
        if base_error and isinstance(exc, base_error):
            return ErrorInfo(502, "upstream_bad_response", "The upstream service returned an invalid response.", error_type=exc.__class__.__name__, retryable=False)

    if PyJWTExceptions:
        if isinstance(exc, tuple(filter(None, (getattr(PyJWTExceptions, "ExpiredSignatureError", None),)))):
            return ErrorInfo(401, "token_expired", default_message(401), error_type=exc.__class__.__name__, retryable=False)
        if isinstance(exc, tuple(filter(None, (getattr(PyJWTExceptions, "ImmatureSignatureError", None),)))):
            return ErrorInfo(401, "token_not_yet_valid", default_message(401), error_type=exc.__class__.__name__, retryable=False)
        if isinstance(exc, tuple(filter(None, (getattr(PyJWTExceptions, "InvalidAudienceError", None), getattr(PyJWTExceptions, "InvalidIssuerError", None), getattr(PyJWTExceptions, "InvalidSubjectError", None), getattr(PyJWTExceptions, "InvalidJTIError", None), getattr(PyJWTExceptions, "MissingRequiredClaimError", None))))):
            return ErrorInfo(401, "invalid_token_claim", default_message(401), error_type=exc.__class__.__name__, retryable=False)
        if isinstance(exc, tuple(filter(None, (getattr(PyJWTExceptions, "DecodeError", None), getattr(PyJWTExceptions, "InvalidSignatureError", None), getattr(PyJWTExceptions, "InvalidTokenError", None))))):
            return ErrorInfo(401, "invalid_token", default_message(401), error_type=exc.__class__.__name__, retryable=False)
        if isinstance(exc, tuple(filter(None, (getattr(PyJWTExceptions, "PyJWKClientConnectionError", None),)))):
            return ErrorInfo(503, "auth_provider_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)

    if BotoCoreExceptions:
        timeout_types = tuple(filter(None, (getattr(BotoCoreExceptions, "ConnectTimeoutError", None), getattr(BotoCoreExceptions, "ReadTimeoutError", None))))
        if timeout_types and isinstance(exc, timeout_types):
            return ErrorInfo(504, "cloud_timeout", default_message(504), error_type=exc.__class__.__name__, retryable=True)
        unavailable_types = tuple(filter(None, (getattr(BotoCoreExceptions, "EndpointConnectionError", None),)))
        if unavailable_types and isinstance(exc, unavailable_types):
            return ErrorInfo(503, "cloud_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)
        credential_types = tuple(filter(None, (getattr(BotoCoreExceptions, "NoCredentialsError", None), getattr(BotoCoreExceptions, "PartialCredentialsError", None))))
        if credential_types and isinstance(exc, credential_types):
            return ErrorInfo(503, "cloud_credentials_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=False)
        param_validation_error = getattr(BotoCoreExceptions, "ParamValidationError", None)
        if param_validation_error and isinstance(exc, param_validation_error):
            return ErrorInfo(422, "invalid_cloud_params", default_message(422), error_type=exc.__class__.__name__, retryable=False)
        client_error = getattr(BotoCoreExceptions, "ClientError", None)
        if client_error and isinstance(exc, client_error):
            response = getattr(exc, "response", {}) or {}
            error = response.get("Error", {}) if isinstance(response, dict) else {}
            error_code = str(error.get("Code", ""))
            message = str(error.get("Message") or default_message(503))
            code_mapping = {
                "AccessDenied": (403, "cloud_forbidden"),
                "AccessDeniedException": (403, "cloud_forbidden"),
                "UnauthorizedOperation": (403, "cloud_forbidden"),
                "NoSuchBucket": (404, "cloud_not_found"),
                "NoSuchKey": (404, "cloud_not_found"),
                "ResourceNotFoundException": (404, "cloud_not_found"),
                "BucketAlreadyExists": (409, "cloud_conflict"),
                "BucketAlreadyOwnedByYou": (409, "cloud_conflict"),
                "ConditionalRequestConflict": (409, "cloud_conflict"),
                "ThrottlingException": (429, "cloud_rate_limited"),
                "RequestLimitExceeded": (429, "cloud_rate_limited"),
                "TooManyRequestsException": (429, "cloud_rate_limited"),
                "ServiceUnavailable": (503, "cloud_unavailable"),
                "InternalFailure": (503, "cloud_unavailable"),
                "RequestTimeout": (504, "cloud_timeout"),
                "RequestExpired": (504, "cloud_timeout"),
                "ValidationError": (422, "invalid_cloud_params"),
                "InvalidParameterValue": (422, "invalid_cloud_params"),
            }
            status_code, code = code_mapping.get(error_code, (503, "cloud_error"))
            return ErrorInfo(status_code, code, message if status_code < 500 else default_message(status_code), _details_with_message(message, aws_error_code=error_code), exc.__class__.__name__, retryable=_upstream_retryable(code))

    if GRPC:
        rpc_error = getattr(GRPC, "RpcError", None)
        if rpc_error and isinstance(exc, rpc_error):
            status = exc.code() if hasattr(exc, "code") else None
            status_code, code = _grpc_status_info(status)
            message = exc.details() if hasattr(exc, "details") else default_message(status_code)
            return ErrorInfo(status_code, code, message or default_message(status_code), error_type=exc.__class__.__name__, retryable=_upstream_retryable(code))

    if GRPCAio:
        aio_rpc_error = getattr(GRPCAio, "AioRpcError", None)
        if aio_rpc_error and isinstance(exc, aio_rpc_error):
            status_code, code = _grpc_status_info(exc.code())
            return ErrorInfo(status_code, code, exc.details() or default_message(status_code), error_type=exc.__class__.__name__, retryable=_upstream_retryable(code))

    elastic_api_error = getattr(ElasticExceptions, "ApiError", None) if ElasticExceptions else None
    if elastic_api_error and isinstance(exc, elastic_api_error):
        status_code = int(getattr(exc, "status_code", 502) or 502)
        final_status, code = _http_error_info(status_code, code_4xx="upstream_invalid_request")
        return ErrorInfo(final_status, code, default_message(final_status), error_type=exc.__class__.__name__, retryable=_upstream_retryable(code))

    elastic_timeout = getattr(ElasticTransportExceptions, "ConnectionTimeout", None) if ElasticTransportExceptions else None
    if elastic_timeout and isinstance(exc, elastic_timeout):
        return ErrorInfo(504, "upstream_timeout", default_message(504), error_type=exc.__class__.__name__, retryable=True)

    elastic_connection = getattr(ElasticTransportExceptions, "ConnectionError", None) if ElasticTransportExceptions else None
    if elastic_connection and isinstance(exc, elastic_connection):
        return ErrorInfo(503, "upstream_unavailable", default_message(503), error_type=exc.__class__.__name__, retryable=True)

    return None
