# -*- coding: utf-8 -*-
# Copyright (C) 2021 GIS OPS UG
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
"""
Core client functionality, common across all routers.
"""


try:
    from . import __version__
except (ModuleNotFoundError, ImportError):
    __version__ = "None"

from abc import ABCMeta, abstractmethod
from datetime import timedelta
from urllib.parse import urlencode

import requests

_DEFAULT_USER_AGENT = "routingpy/v{}".format(__version__)
_RETRIABLE_STATUSES = set([503])


class options(object):
    """
    Contains default configuration options for all routers, e.g. `timeout` and `proxies`. Each router can take the same
    configuration values, which will override the values set in `options` object.

    Example for overriding default values for `user_agent` and proxies:

    >>> from routingpy.routers import options
    >>> from routingpy.routers import MapboxOSRM
    >>> options.default_user_agent = 'amazing_routing_app'
    >>> options.default_proxies = {'https': '129.125.12.0'}
    >>> router = MapboxOSRM(my_key)
    >>> print(router.client.headers)
    {'User-Agent': 'amazing_routing_app', 'Content-Type': 'application/x-www-form-urlencoded'}
    >>> print(router.client.proxies)
    {'https': '129.125.12.0'}

    Attributes:
        self.default_timeout:
            Combined connect and read timeout for HTTP requests, in
            seconds. Specify "None" for no timeout. Integer.

        self.default_retry_timeout:
            Timeout across multiple retriable requests, in
            seconds. Integer.

        self.default_retry_over_query_limit:
            If True, client will not raise an exception
            on HTTP 429, but instead jitter a sleeping timer to pause between
            requests until HTTP 200 or retry_timeout is reached. Boolean.

        self.default_skip_api_error:
            Continue with batch processing if a :class:`routingpy.exceptions.RouterApiError` is
            encountered (e.g. no route found). If False, processing will discontinue and raise an error. Boolean.

        self.default_user_agent:
            User-Agent to send with the requests to routing API. String.

        self.default_proxies:
            Proxies passed to the requests library. Dictionary.
    """

    default_timeout = 60
    default_retry_timeout = 60
    default_retry_over_query_limit = True
    default_skip_api_error = False
    default_user_agent = _DEFAULT_USER_AGENT
    default_proxies = None


# To avoid trouble when respecting timeout for individual routers (i.e. can't be None, since that's no timeout)
DEFAULT = type("object", (object,), {"__repr__": lambda self: "DEFAULT"})()


class BaseClient(metaclass=ABCMeta):
    """Abstract base class every client inherits from. Authentication is handled in each subclass."""

    def __init__(
        self,
        base_url,
        user_agent=None,
        timeout=DEFAULT,
        retry_timeout=None,
        retry_over_query_limit=None,
        skip_api_error=None,
        **kwargs
    ):
        """
        :param base_url: The base URL for the request. All routers must provide a default.
            Should not have a trailing slash.
        :type base_url: string

        :param user_agent: User-Agent to send with the requests to routing API.
            Overrides ``options.default_user_agent``.
        :type user_agent: string

        :param timeout: Combined connect and read timeout for HTTP requests, in
            seconds. Specify "None" for no timeout.
        :type timeout: int

        :param retry_timeout: Timeout across multiple retriable requests, in
            seconds.
        :type retry_timeout: int

        :param retry_over_query_limit: If True, client will not raise an exception
            on HTTP 429, but instead jitter a sleeping timer to pause between
            requests until HTTP 200 or retry_timeout is reached.
        :type retry_over_query_limit: bool

        :param skip_api_error: Continue with batch processing if a :class:`routingpy.exceptions.RouterApiError` is
            encountered (e.g. no route found). If False, processing will discontinue and raise an error. Default False.
        :type skip_api_error: bool

        :param **kwargs: Additional keyword arguments.
        :type **kwargs: dict
        """
        self.base_url = base_url

        self.retry_over_query_limit = (
            retry_over_query_limit
            if retry_over_query_limit is False
            else options.default_retry_over_query_limit
        )
        self.retry_timeout = timedelta(seconds=retry_timeout or options.default_retry_timeout)

        self.skip_api_error = skip_api_error or options.default_skip_api_error

        self.headers = {
            "User-Agent": user_agent or options.default_user_agent,
            "Content-Type": "application/json",
        }

        self.timeout = timeout if timeout != DEFAULT else options.default_timeout

        self.kwargs = kwargs

        self._req = None

    @abstractmethod
    def _request(
        self,
        url,
        get_params={},
        post_params=None,
        first_request_time=None,
        retry_counter=0,
        dry_run=None,
    ):
        """Performs HTTP GET/POST with credentials, returning the body as
        JSON.

        :param url: URL path for the request. Should begin with a slash.
        :type url: string

        :param get_params: HTTP GET parameters.
        :type get_params: dict or list of tuples

        :param post_params: HTTP POST parameters. Only specified by calling method.
        :type post_params: dict

        :param first_request_time: The time of the first request (None if no
            retries have occurred).
        :type first_request_time: :class:`datetime.datetime`

        :param retry_counter: The number of this retry, or zero for first attempt.
        :type retry_counter: int

        :param dry_run: If true, only prints URL and parameters. true or false.
        :type dry_run: bool

        :raises routingpy.exceptions.RouterApiError: when the API returns an error due to faulty configuration.
        :raises routingpy.exceptions.RouterServerError: when the API returns a server error.
        :raises routingpy.exceptions.RouterError: when anything else happened while requesting.
        :raises routingpy.exceptions.JSONParseError: when the JSON response can't be parsed.
        :raises routingpy.exceptions.Timeout: when the request timed out.
        :raises routingpy.exceptions.TransportError: when something went wrong while trying to
            execute a request.

        :returns: raw JSON response.
        :rtype: dict
        """
        pass

    @staticmethod
    def _generate_auth_url(path, params):
        """Returns the path and query string portion of the request URL, first
        adding any necessary parameters.

        :param path: The path portion of the URL.
        :type path: string

        :param params: URL parameters.
        :type params: dict or list of key/value tuples

        :rtype: string

        """

        if not params:
            return path

        if isinstance(params, dict):
            params = sorted(dict(**params).items())
        elif isinstance(params, (list, tuple)):
            params = params

        return path + "?" + requests.utils.unquote_unreserved(urlencode(params))
