"""
Common generated classes shared across all resources.

The version of the OpenAPI document: 0.0.1
Contact: support@snowflake.com
Generated by: https://openapi-generator.tech

Do not edit this file manually.
"""

import json
import logging
import mimetypes
import os
import random
import re
import time
import typing

from functools import partial
from urllib.parse import quote

import snowflake.core._http_requests

from snowflake.core._generated import rest
from snowflake.core._generated.configuration import Configuration
from snowflake.core._generated.paging import PagedIter
from snowflake.core._thread_pool import get_thread_pool
from snowflake.core.exceptions import APIError, InvalidResponseError, RetryTimeoutError, _APIValueError
from snowflake.core.version import __version__ as VERSION


if typing.TYPE_CHECKING:
    from snowflake.core._root import Root


logger = logging.getLogger(__name__)


class ApiClient:
    """Generic API client for OpenAPI client library builds.

    OpenAPI generic API client. This client handles the client-
    server communication, and is invariant across implementations. Specifics of
    the methods and models for each application are generated from the OpenAPI
    templates.

    NOTE: This class is auto generated by OpenAPI Generator.
    Ref: https://openapi-generator.tech
    Do not edit the class manually.

    :param configuration: .Configuration object for this client
    :param header_name: a header to pass when making calls to the API.
    :param header_value: a header value to pass when making calls to
        the API.
    :param cookie: a cookie to include in the header when making calls
        to the API
    """

    def __init__(self, configuration=None, header_name=None, header_value=None, cookie=None):
        # use default configuration if none is provided
        if configuration is None:
            configuration = Configuration.get_default()
        self.configuration = configuration

        self.rest_client = rest.RESTClientObject(configuration)
        self.default_headers = {}
        if header_name is not None:
            self.default_headers[header_name] = header_value
        self.cookie = cookie
        # Set default User-Agent.
        self.user_agent = "python_api/" + VERSION + ""
        self.client_side_validation = configuration.client_side_validation

    @property
    def user_agent(self):
        """User agent for this API client"""
        return self.default_headers["User-Agent"]

    @user_agent.setter
    def user_agent(self, value):
        self.default_headers["User-Agent"] = value

    def set_default_header(self, header_name, header_value):
        self.default_headers[header_name] = header_value

    @staticmethod
    def get_host(root: "Root") -> str:
        if (
            hasattr(root, "connection")
            and root.connection is not None
            and hasattr(root.connection, "rest")
            and root.connection.rest is not None
            and hasattr(root.connection.rest, "_protocol")
            and root._hostname is not None
            and hasattr(root.connection.rest, "_port")
        ):
            return f"{root.connection.rest._protocol}://{root._hostname}:{root.connection.rest._port}"
        # for spark connect's RESTRoot
        if hasattr(root, "_restConnection") and root._restConnection is not None:
            return root._restConnection.url
        return "https://org-account.snowflakecomputing.com"  # default stub value

    _default = None

    @classmethod
    def get_default(cls):
        """Return new instance of ApiClient.

        This method returns newly created, based on default constructor,
        object of ApiClient class or returns a copy of default
        ApiClient.

        :return: The ApiClient object.
        """
        if cls._default is None:
            cls._default = ApiClient()
        return cls._default

    @classmethod
    def set_default(cls, default):
        """Set default instance of ApiClient.

        It stores default ApiClient.

        :param default: object of ApiClient.
        """
        cls._default = default

    def __call_api(
        self,
        root,
        resource_path,
        method,
        path_params=None,
        query_params=None,
        header_params=None,
        body=None,
        post_params=None,
        files=None,
        response_types_map=None,
        auth_settings=None,
        _return_http_data_only=None,
        collection_formats=None,
        _preload_content=True,
        _request_timeout=None,
        _host=None,
        _request_auth=None,
        _deserialize_response_fn=None,
    ):
        # header parameters
        header_params = header_params or {}
        user_agents = ["python_api/" + VERSION + ""]
        if root.root_config.has_user_agents():
            user_agents.append(root.root_config.get_user_agents())
        self.user_agent = " ".join(user_agents)
        header_params.update(self.default_headers)
        if self.cookie:
            header_params["Cookie"] = self.cookie
        if header_params:
            header_params = snowflake.core._http_requests.sanitize_for_serialization(header_params)
            header_params = dict(snowflake.core._http_requests.parameters_to_tuples(header_params, collection_formats))

        # post parameters
        if post_params or files:
            post_params = post_params if post_params else []
            post_params = snowflake.core._http_requests.sanitize_for_serialization(post_params)
            post_params = snowflake.core._http_requests.parameters_to_tuples(post_params, collection_formats)
            post_params.extend(self.files_parameters(files))

        # auth setting
        self.update_params_for_auth(
            header_params, query_params, auth_settings, resource_path, method, body, request_auth=_request_auth
        )

        # body
        if body:
            body = snowflake.core._http_requests.sanitize_for_serialization(body)

        # request url
        if _host is None:
            url = self.get_host(root) + resource_path
        else:
            # use server/host defined in path or operation instead
            url = _host + resource_path

        # query parameters
        if query_params:
            query_params = snowflake.core._http_requests.sanitize_for_serialization(query_params)
            url_query = self.parameters_to_url_query(query_params, collection_formats)
            url += "?" + url_query

        try:
            # perform request and return response, maybe with retry
            response_data = self.request_with_retry(
                root,
                method,
                url,
                query_params=query_params,
                headers=header_params,
                post_params=post_params,
                body=body,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
            )
        except APIError as e:
            if e.body:
                e.body = e.body.decode("utf-8")
            raise e

        self.last_response = response_data

        return_data = response_data

        if not _preload_content:
            return return_data

        response_type = response_types_map.get(str(response_data.status), None)

        # if response_type not in ["file", "bytes"]:
        #     match = None
        #     content_type = response_data.getheader('content-type')
        #     if content_type is not None:
        #         match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
        #     encoding = match.group(1) if match else "utf-8"
        #     response_data.data = response_data.data.decode(encoding)

        # deserialize response data

        if response_type:
            large_results_data = ApiClient.large_results(response_data)
            if large_results_data is None:
                # regular, non-large results use case
                return_data = (
                    _deserialize_response_fn(response_data, response_type)
                    if _deserialize_response_fn
                    else response_data
                )
            else:
                # Closure for fetching the next chunk in the PagedIter iterator
                def _fetch_next_chunk(chunk_index, deserialize_type):
                    results_path = large_results_data[0]

                    # The first page may already be in the response body, if it is not just a
                    # message informing us of the large results.
                    if chunk_index == 0 and not ApiClient.has_large_results_message_in_body(response_data.data):
                        chunk_response_data = response_data
                    else:
                        # For now, do this because query_params is not actually being used properly in self.request
                        chunk_url = f"{self.get_host(root)}{results_path}?page={chunk_index}"

                        chunk_response_data = self.request_with_retry(
                            root,
                            "GET",
                            chunk_url,
                            headers=header_params,
                            _preload_content=True,
                            _request_timeout=_request_timeout,
                            query_params={"page": chunk_index},
                        )

                    return (
                        _deserialize_response_fn(chunk_response_data, deserialize_type)
                        if _deserialize_response_fn
                        else chunk_response_data
                    )

                if "Iterable" in response_type:
                    return PagedIter(
                        partial(_fetch_next_chunk, deserialize_type=response_type),
                        number_of_chunks_=large_results_data[1],
                    )
                else:
                    # At most, we should only need to fetch one chunk if it's a point lookup,
                    # i.e., one row return
                    return_data = _fetch_next_chunk(0, response_type)
        else:
            return_data = None

        if _return_http_data_only:
            return return_data
        else:
            return (return_data, response_data.status, response_data.getheaders())

    def call_api(
        self,
        root,
        resource_path,
        method,
        path_params=None,
        query_params=None,
        header_params=None,
        body=None,
        post_params=None,
        files=None,
        response_types_map=None,
        auth_settings=None,
        async_req=None,
        _return_http_data_only=None,
        collection_formats=None,
        _preload_content=True,
        _request_timeout=None,
        _host=None,
        _request_auth=None,
        _deserialize_response_fn=None,
    ):
        """Makes the HTTP request (synchronous) and returns deserialized data.

        To make an async_req request, set the async_req parameter.

        :param resource_path: Path to method endpoint.
        :param method: Method to call.
        :param path_params: Path parameters in the url.
        :param query_params: Query parameters in the url.
        :param header_params: Header parameters to be
            placed in the request header.
        :param body: Request body.
        :param post_params dict: Request post form parameters,
            for `application/x-www-form-urlencoded`, `multipart/form-data`.
        :param auth_settings list: Auth Settings names for the request.
        :param response: Response data type.
        :param files dict: key -> filename, value -> filepath,
            for `multipart/form-data`.
        :param async_req bool: execute request asynchronously
        :param _return_http_data_only: response data without head status code
                                       and headers
        :param collection_formats: dict of collection formats for path, query,
            header, and post parameters.
        :param _preload_content: if False, the urllib3.HTTPResponse object will
                                 be returned without reading/decoding response
                                 data. Default is True.
        :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.
        :param _request_auth: set to override the auth_settings for an a single
                              request; this effectively ignores the authentication
                              in the spec for a single request.
        :param _deserialize_response_fn: function to deserialize the response data
        :type _request_token: dict, optional
        :return:
            If async_req parameter is True,
            the request will be called asynchronously.
            The method will return the request thread.
            If parameter async_req is False or missing,
            then the method will return the response directly.
        """
        # Insert path parameters to fully resolve URL before logging
        resource_path = snowflake.core._http_requests.resolve_url(
            resource_path,
            path_params,
            collection_formats,
            self.configuration.safe_chars_for_path_param,
        )
        logger.info(
            "performing %s HTTP %s call to %s",
            "an async" if async_req else "a",
            method,
            resource_path,
        )
        logger.debug(
            "request has path params: %r, query_params: %r, header params: %r and body: %r",
            path_params,
            query_params,
            header_params,
            body,
        )
        if not async_req:
            return self.__call_api(
                root,
                resource_path,
                method,
                path_params,
                query_params,
                header_params,
                body,
                post_params,
                files,
                response_types_map,
                auth_settings,
                _return_http_data_only,
                collection_formats,
                _preload_content,
                _request_timeout,
                _host,
                _request_auth,
                _deserialize_response_fn,
            )

        pool = get_thread_pool(root.parameters().max_threads)
        return pool.submit(
            self.__call_api,
            *(
                root,
                resource_path,
                method,
                path_params,
                query_params,
                header_params,
                body,
                post_params,
                files,
                response_types_map,
                auth_settings,
                _return_http_data_only,
                collection_formats,
                _preload_content,
                _request_timeout,
                _host,
                _request_auth,
                _deserialize_response_fn,
            ),
        )

    def request_with_retry(
        self,
        root,
        method,
        url,
        query_params=None,
        headers=None,
        post_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        """Response time by default one hour"""
        enter_timing = time.time()
        response_data = self.request(
            root,
            method,
            url,
            query_params=query_params,
            headers=headers,
            post_params=post_params,
            body=body,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
        )

        if not root.parameters().should_retry_request or response_data.status not in rest.RETRY_STATUS_CODES:
            return response_data

        logger.info(
            f"Request Status {response_data.status} ({snowflake.core._http_requests.STATUS_CODES_MAPPING.get(response_data.status)}) Retrying..."
        )
        # Query retry support
        result_endpoint = response_data.getheader("Location")
        if response_data.status == 202 and result_endpoint is None:
            raise InvalidResponseError(
                f"{snowflake.core._http_requests.STATUS_CODES_MAPPING.get(response_data.status)} result endpoint is missing"
            )

        if _request_timeout is None:
            _request_timeout = snowflake.core._http_requests.DEFAULT_RETRY_TIMEOUT_SECONDS
        wait_for_results_timeout = enter_timing + _request_timeout

        exponential_wait_time = 1  # wait time increases exponentially, 30% more everytime
        while True:
            time_remaining = wait_for_results_timeout - time.time()
            if time_remaining <= 0:
                break
            # Full Jitter
            # ref: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
            # Take max(1, wait_time) to avoid making too many requests in a short period
            wait_time = max(1, random.randint(0, int(min(exponential_wait_time, time_remaining))))

            if response_data.status in rest.RETRY_STATUS_CODES:
                logger.info(
                    f"{snowflake.core._http_requests.STATUS_CODES_MAPPING.get(response_data.status)} detected. Sleeping for {wait_time} seconds before checking again"
                )

                time.sleep(wait_time)

                new_url = url
                if response_data.status == 202:
                    new_url = self.get_host(root) + result_endpoint
                    method = "GET"

                response_data = self.request(
                    root,
                    method,
                    new_url,
                    query_params=query_params,
                    headers=headers,
                    post_params=post_params,
                    body=body,
                    _preload_content=_preload_content,
                    _request_timeout=max(time_remaining - wait_time, 1),
                    # request_timeout can never be zero
                )
            else:
                logger.info(
                    f"{snowflake.core._http_requests.STATUS_CODES_MAPPING.get(response_data.status)} query completed"
                )
                return response_data

            exponential_wait_time *= 1.3

        raise RetryTimeoutError(
            f"{response_data.status} {snowflake.core._http_requests.STATUS_CODES_MAPPING.get(response_data.status)} query timeout"
        )

    def request(
        self,
        root,
        method,
        url,
        query_params=None,
        headers=None,
        post_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        """Makes the HTTP request using RESTClient."""
        if method == "GET":
            return self.rest_client.get_request(
                root,
                url,
                query_params=query_params,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
                headers=headers,
            )
        elif method == "HEAD":
            return self.rest_client.head_request(
                root,
                url,
                query_params=query_params,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
                headers=headers,
            )
        elif method == "OPTIONS":
            return self.rest_client.options_request(
                root,
                url,
                query_params=query_params,
                headers=headers,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
            )
        elif method == "POST":
            return self.rest_client.post_request(
                root,
                url,
                query_params=query_params,
                headers=headers,
                post_params=post_params,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
                body=body,
            )
        elif method == "PUT":
            try:
                return self.rest_client.put_request(
                    root,
                    url,
                    query_params=query_params,
                    headers=headers,
                    post_params=post_params,
                    _preload_content=_preload_content,
                    _request_timeout=_request_timeout,
                    body=body,
                )
            except APIError as error:
                # Raise a more helpful user error if CoA is not supported for this resource;
                # this is represented as either 405 or 501 on the server.
                if error.status in (405, 501):
                    raise NotImplementedError("create_or_update is not yet supported for this resource.")
                raise

        elif method == "PATCH":
            return self.rest_client.patch_request(
                root,
                url,
                query_params=query_params,
                headers=headers,
                post_params=post_params,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
                body=body,
            )
        elif method == "DELETE":
            return self.rest_client.delete_request(
                root,
                url,
                query_params=query_params,
                headers=headers,
                _preload_content=_preload_content,
                _request_timeout=_request_timeout,
                body=body,
            )
        else:
            raise _APIValueError("http method must be `GET`, `HEAD`, `OPTIONS`, `POST`, `PATCH`, `PUT` or `DELETE`.")

    def parameters_to_url_query(self, params, collection_formats):
        """Get parameters as list of tuples, formatting collections.

        :param params: Parameters as dict or list of two-tuples
        :param dict collection_formats: Parameter collection formats
        :return: URL query string (e.g. a=Hello%20World&b=123)
        """
        new_params = []
        if collection_formats is None:
            collection_formats = {}
        for k, v in params.items() if isinstance(params, dict) else params:
            if isinstance(v, (int, float)):
                v = str(v)
            if isinstance(v, bool):
                v = str(v).lower()

            if k in collection_formats:
                collection_format = collection_formats[k]
                if collection_format == "multi":
                    new_params.extend((k, value) for value in v)
                else:
                    if collection_format == "ssv":
                        delimiter = " "
                    elif collection_format == "tsv":
                        delimiter = "\t"
                    elif collection_format == "pipes":
                        delimiter = "|"
                    else:  # csv is the default
                        delimiter = ","
                    new_params.append((k, delimiter.join(quote(str(value)) for value in v)))
            else:
                new_params.append((k, v))

        return "&".join(["=".join(map(str, item)) for item in new_params])

    def files_parameters(self, files=None):
        """Builds form parameters.

        :param files: File parameters.
        :return: Form parameters with files.
        """
        params = []

        if files:
            for k, v in files.items():
                if not v:
                    continue
                file_names = v if type(v) is list else [v]
                for n in file_names:
                    with open(n, "rb") as f:
                        filename = os.path.basename(f.name)
                        filedata = f.read()
                        mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
                        params.append(tuple([k, tuple([filename, filedata, mimetype])]))

        return params

    def select_header_accept(self, accepts):
        """Returns `Accept` based on an array of accepts provided.

        :param accepts: List of headers.
        :return: Accept (e.g. application/json).
        """
        if not accepts:
            return

        for accept in accepts:
            if re.search("json", accept, re.IGNORECASE):
                return accept

        return accepts[0]

    def select_header_content_type(self, content_types):
        """Returns `Content-Type` based on an array of content_types provided.

        :param content_types: List of content-types.
        :return: Content-Type (e.g. application/json).
        """
        if not content_types:
            return None

        for content_type in content_types:
            if re.search("json", content_type, re.IGNORECASE):
                return content_type

        return content_types[0]

    def update_params_for_auth(self, headers, queries, auth_settings, resource_path, method, body, request_auth=None):
        """Updates header and query params based on authentication setting.

        :param headers: Header parameters dict to be updated.
        :param queries: Query parameters tuple list to be updated.
        :param auth_settings: Authentication setting identifiers list.
        :resource_path: A string representation of the HTTP request resource path.
        :method: A string representation of the HTTP request method.
        :body: A object representing the body of the HTTP request.
        The object type is the return value of sanitize_for_serialization().
        :param request_auth: if set, the provided settings will
                             override the token in the configuration.
        """
        if not auth_settings:
            return

        if request_auth:
            self._apply_auth_params(headers, queries, resource_path, method, body, request_auth)
            return

        for auth in auth_settings:
            auth_setting = self.configuration.auth_settings().get(auth)
            if auth_setting:
                self._apply_auth_params(headers, queries, resource_path, method, body, auth_setting)

    def _apply_auth_params(self, headers, queries, resource_path, method, body, auth_setting):
        """Updates the request parameters based on a single auth_setting

        :param headers: Header parameters dict to be updated.
        :param queries: Query parameters tuple list to be updated.
        :resource_path: A string representation of the HTTP request resource path.
        :method: A string representation of the HTTP request method.
        :body: A object representing the body of the HTTP request.
        The object type is the return value of sanitize_for_serialization().
        :param auth_setting: auth settings for the endpoint
        """
        if auth_setting["in"] == "cookie":
            headers["Cookie"] = auth_setting["value"]
        elif auth_setting["in"] == "header":
            if auth_setting["type"] != "http-signature":
                headers[auth_setting["key"]] = auth_setting["value"]

        elif auth_setting["in"] == "query":
            queries.append((auth_setting["key"], auth_setting["value"]))
        else:
            raise _APIValueError("Authentication token must be in `query` or `header`")

    @staticmethod
    def has_large_results_message_in_body(body):
        try:
            result = json.loads(body)
            return (
                "result_handler" in result
                and "message" in result
                and "Large result set. Use provided Link" in result["message"]
            )
        except ValueError:
            return False

    @staticmethod
    def large_results(response):
        """Returns a tuple of the result URL and number of chunks; returns None if no large results are detected.

        :param response: A RESTResponse object in which to check for large results.
        :return: A Tuple of (results_url, number_of_chunks) if there are large results, or else None.
        """
        # This should be the normal way in which we figure out where to get the results from,
        # as well as how many chunks there are to get: check for "Links" in the headers.
        if "Link" in response.getheaders():
            return ApiClient.get_path_and_chunk_count_from_header(response.getheaders()["Link"])

        # Due to a bug, we use the alternate logic to infer the URL from the UUID
        if ApiClient.has_large_results_message_in_body(response.data):
            result = json.loads(response.data)

            handler_id = result["result_handler"]
            results_path = "/api/v2/results/" + handler_id

            # If there is no "Link" header, there is just one chunk
            num_chunks = 1

            return results_path, num_chunks

        return None

    @staticmethod
    def get_path_and_chunk_count_from_header(links_str):
        links_list = links_str.split(",")

        def parse_links(s):
            import re

            # Use regex to extract necessary parts
            #
            # Explanation:
            # The result links are provided to us in the form:
            # '</api/v2/results/01b39664-0100-0001-0000-0000000430391?page=0>; rel="first"'
            #
            # We wish to find how many chunks there are by finding the one that has rel="last",
            # therefore we find page index corresponding to the link with that rel value.
            #
            # 1. <(.*?)> matches and captures the URL inside the angle brackets
            # 2. page=(\d+) matches 'page=' followed by one or more digits to find the page number
            # 3. rel="([^"]*)" matches 'rel="'
            pattern = r'<(.*?)\?page=(\d+)>; rel="([^"]*)"'

            # Search using the regular expression
            match = re.search(pattern, s)
            if match:
                parse_result = dict()
                parse_result["url"], parse_result["page_number"], parse_result["rel_value"] = match.groups()
                return parse_result

            return None

        parsed_links = [parse_links(link) for link in links_list]

        # Find the last one
        last_link = list(filter(lambda link: link["rel_value"].lower() == "last", parsed_links)).pop()

        # Return the URL; the number of chunks is the chunk index of the last page plus one
        return last_link["url"], int(last_link["page_number"]) + 1


class StoredProcApiClient(ApiClient):
    def __init__(self, root, configuration=None, header_name=None, header_value=None, cookie=None):
        ApiClient.__init__(self, configuration, header_name, header_value, cookie)
        self.rest_client = rest.StoredProcClientObject()
