# coding: utf-8

"""
    Rapidata Asset API

    The API for the Rapidata Asset service

    The version of the OpenAPI document: v1
    Generated by OpenAPI Generator (https://openapi-generator.tech)

    Do not edit the class manually.
"""  # noqa: E501

import io
import json
import logging
import re
import time
from typing import Dict, Optional

import httpx
from authlib.integrations.httpx_client import OAuth2Client
from httpx import Timeout, ConnectError, Limits, RemoteProtocolError, ConnectTimeout, ReadTimeout

from rapidata.api_client.exceptions import ApiException, ApiValueError

_logger = logging.getLogger("rapidata.api_client")
_TRANSIENT_RETRY_MAX_ATTEMPTS = 3
_TRANSIENT_RETRY_BASE_DELAY = 0.5
_RETRYABLE_STATUS_CODES = {502, 503, 504}


class RESTResponse(io.IOBase):

    def __init__(self, resp: httpx.Response) -> None:
        self.response = resp
        self.status = resp.status_code
        self.reason = resp.reason_phrase
        self.data = None

    def read(self):
        if self.data is None:
            self.data = self.response.content
        return self.data

    def getheaders(self) -> Dict[str, str]:
        """Returns a dictionary of the response headers."""
        return dict(self.response.headers)

    def getheader(self, name, default=None) -> Optional[str]:
        """Returns a given response header."""
        return self.response.headers.get(name, default)


RESTResponseType = RESTResponse


class RESTClientObject:

    def __init__(self, configuration) -> None:
        self.configuration = configuration

        self.session: Optional[OAuth2Client] = None

    def setup_oauth_client_credentials(
        self, client_id: str, client_secret: str, token_endpoint: str, scope: str
    ):
        client_args = self._get_session_defaults()
        self.session = OAuth2Client(
            client_id=client_id,
            client_secret=client_secret,
            token_endpoint=token_endpoint,
            scope=scope,
            **client_args,
        )

        try:
            self.session.fetch_token()
        except ConnectError as e:
            if self._is_certificate_validation_error(e):
                exit(self._get_ssl_verify_error_message())
            else:
                raise

    def setup_oauth_with_token(
        self,
        client_id: str | None,
        client_secret: str | None,
        token: dict,
        token_endpoint: str,
        leeway: int = 60,
    ):
        client_args = self._get_session_defaults()
        self.session = OAuth2Client(
            token=token,
            token_endpoint=token_endpoint,
            client_id=client_id,
            client_secret=client_secret,
            leeway=leeway,
            **client_args,
        )

    def request(
        self,
        method,
        url,
        headers=None,
        body=None,
        post_params=None,
        _request_timeout=None,
    ):
        """Perform requests.

        :param method: http request method
        :param url: http request url
        :param headers: http request headers
        :param body: request json body, for `application/json`
        :param post_params: request post parameters,
                            `application/x-www-form-urlencoded`
                            and `multipart/form-data`
        :param _request_timeout: timeout setting for this request. If one
                                 number provided, it will be total request
                                 timeout. It can also be a pair (tuple) of
                                 (connection, read) timeouts.
        """
        method = method.upper()
        assert method in ["GET", "HEAD", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"]

        if post_params and body:
            raise ApiValueError(
                "body parameter cannot be used with post_params parameter."
            )

        post_params = post_params or {}
        headers = headers or {}

        if not self.session:
            raise ApiValueError(
                "OAuth2 session is not initialized. Please initialize it before making requests."
            )

        timeout = self._build_timeout(_request_timeout)

        for attempt in range(_TRANSIENT_RETRY_MAX_ATTEMPTS + 1):
            try:
                r = self._send_request(self.session, method, url, headers, body, post_params, timeout)
            except httpx.HTTPError as e:
                self._handle_http_error(e, method, url, attempt)
                continue

            if r.status_code in _RETRYABLE_STATUS_CODES and attempt < _TRANSIENT_RETRY_MAX_ATTEMPTS:
                delay = _TRANSIENT_RETRY_BASE_DELAY * (2 ** attempt)
                _logger.warning(
                    "Server error on %s %s (attempt %d/%d): %d. Retrying in %.1fs...",
                    method, url, attempt + 1, _TRANSIENT_RETRY_MAX_ATTEMPTS + 1,
                    r.status_code, delay,
                )
                time.sleep(delay)
                continue

            return RESTResponse(r)

        raise ApiException(status=0, reason="All retry attempts exhausted")

    def _send_request(self, session, method, url, headers, body, post_params, timeout):
        """Dispatch the HTTP request based on method and content type."""
        if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]:
            return self._send_body_request(session, method, url, headers, body, post_params, timeout)
        return session.request(method, url, timeout=timeout, headers=headers)

    @staticmethod
    def _send_body_request(session, method, url, headers, body, post_params, timeout):
        """Send a request that may include a body."""
        content_type = headers.get("Content-Type")

        if not content_type or re.search("json", content_type, re.IGNORECASE):
            request_body = json.dumps(body) if body is not None else None
            return session.request(method, url, content=request_body, timeout=timeout, headers=headers)

        if content_type == "application/x-www-form-urlencoded":
            return session.request(method, url, data=post_params, timeout=timeout, headers=headers)

        if content_type == "multipart/form-data":
            headers = headers.copy()
            del headers["Content-Type"]
            files, data = RESTClientObject._parse_multipart_params(post_params)
            return session.request(method, url, files=files, data=data, timeout=timeout, headers=headers)

        if isinstance(body, (str, bytes)):
            return session.request(method, url, content=body, timeout=timeout, headers=headers)

        if headers["Content-Type"].startswith("text/") and isinstance(body, bool):
            request_body = "true" if body else "false"
            return session.request(method, url, content=request_body, timeout=timeout, headers=headers)

        msg = """Cannot prepare a request message for provided arguments.
                 Please check that your arguments match declared content type."""
        raise ApiException(status=0, reason=msg)

    @staticmethod
    def _parse_multipart_params(post_params):
        """Parse post parameters into files and data for multipart upload."""
        files = []
        data = {}
        for key, value in post_params:
            if isinstance(value, tuple) and len(value) >= 2:
                filename, file_data = value[0], value[1]
                content_type = value[2] if len(value) > 2 else None
                files.append((key, (filename, file_data, content_type)))
            elif isinstance(value, dict):
                if key in data:
                    if not isinstance(data[key], list):
                        data[key] = [data[key]]
                    data[key].append(json.dumps(value))
                else:
                    data[key] = json.dumps(value)
            else:
                if key in data:
                    if not isinstance(data[key], list):
                        data[key] = [data[key]]
                    data[key].append(value)
                else:
                    data[key] = value
        return files, data

    @staticmethod
    def _build_timeout(_request_timeout):
        """Build a Timeout object from the request timeout parameter."""
        if not _request_timeout:
            return None
        if isinstance(_request_timeout, (int, float)):
            return Timeout(timeout=_request_timeout)
        if isinstance(_request_timeout, tuple) and len(_request_timeout) == 2:
            connect_timeout, read_timeout = _request_timeout
            return Timeout(timeout=connect_timeout, read=read_timeout)
        return None

    def _handle_http_error(self, error, method, url, attempt):
        """Handle HTTP errors, retrying transient failures with exponential backoff.

        Returns normally to signal a retry. Raises ApiException for permanent failures.
        """
        if isinstance(error, ConnectError) and self._is_certificate_validation_error(error):
            raise ApiException(
                status=0,
                reason="SSL Certificate Verification Failed\n"
                + self._get_ssl_verify_error_message(),
            )

        if self._is_retryable_error(error) and attempt < _TRANSIENT_RETRY_MAX_ATTEMPTS:
            delay = _TRANSIENT_RETRY_BASE_DELAY * (2 ** attempt)
            _logger.warning(
                "Transient network error on %s %s (attempt %d/%d): %s. Retrying in %.1fs...",
                method, url, attempt + 1, _TRANSIENT_RETRY_MAX_ATTEMPTS + 1,
                type(error).__name__, delay,
            )
            time.sleep(delay)
            return

        msg = "\n".join([type(error).__name__, str(error)])
        raise ApiException(status=0, reason=msg)

    def _get_session_defaults(self):
        # Set connection pool limits to support high concurrency uploads
        limits = Limits(max_connections=200, max_keepalive_connections=200)

        client_kwargs = {
            "verify": (
                self.configuration.ssl_ca_cert
                if self.configuration.ssl_ca_cert
                else self.configuration.verify_ssl
            ),
            "limits": limits,
        }

        if self.configuration.proxy:
            client_kwargs["proxy"] = self.configuration.proxy

            existing_headers = client_kwargs.pop("headers")
            if self.configuration.proxy_headers:
                for key, value in self.configuration.proxy_headers.items():
                    existing_headers[key] = value
            client_kwargs["headers"] = existing_headers

        if self.configuration.retries is not None:
            transport = httpx.HTTPTransport(retries=self.configuration.retries, limits=limits)
            client_kwargs["transport"] = transport

        return client_kwargs

    @staticmethod
    def _is_retryable_error(error: httpx.HTTPError) -> bool:
        """Check if the error is a transient network error safe to retry."""
        if isinstance(error, ConnectError):
            if RESTClientObject._is_certificate_validation_error(error):
                return False
            return True
        return isinstance(error, (RemoteProtocolError, ConnectTimeout, ReadTimeout))

    @staticmethod
    def _is_certificate_validation_error(error: ConnectError) -> bool:
        """
        Check if the error is related to certificate validation.
        """
        return error.args[0].startswith("[SSL: CERTIFICATE_VERIFY_FAILED]")

    @staticmethod
    def _get_ssl_verify_error_message() -> str:
        return """
        We encountered an issue while trying to verify the SSL certificate.
        This often happens on macOS when using the default Python installation provided by Apple,
        which lacks the required certificates to perform secure HTTPS requests.
        To resolve this, please perform the following steps:
        1. Make sure you are using the latest version of the Rapidata Package: `pip install --upgrade rapidata`
        2. If you are using the default Python installation from Apple, consider switching to a different Python distribution, such as Homebrew or pyenv.
        3. If you prefer to continue using the default Python, you can run the following command in your terminal:
           `/Applications/Python\\ 3.1*/Install\\ Certificates.command`
        
        For more details on this issue, please refer to the following link:
        https://stackoverflow.com/questions/42098126/mac-osx-python-ssl-sslerror-ssl-certificate-verify-failed-certificate-verify
        
        If the issue persists, please reach out to us at 'info@rapidata.ai', we're happy to help you.
        """

