#!/usr/bin/env python
#
# Copyright 2016 - The Android Open Source Project
#
# 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.

"""Base Cloud API Client.

BasicCloudApiCliend does basic setup for a cloud API.
"""
import httplib
import logging
import os
import socket
import ssl

from apiclient import errors as gerrors
from apiclient.discovery import build
import apiclient.http
import httplib2
from oauth2client import client

from acloud.internal.lib import utils
from acloud.public import errors

logger = logging.getLogger(__name__)


class BaseCloudApiClient(object):
    """A class that does basic setup for a cloud API."""

    # To be overriden by subclasses.
    API_NAME = ""
    API_VERSION = "v1"
    SCOPE = ""

    # Defaults for retry.
    RETRY_COUNT = 5
    RETRY_BACKOFF_FACTOR = 1.5
    RETRY_SLEEP_MULTIPLIER = 2
    RETRY_HTTP_CODES = [
        # 403 is to retry the "Rate Limit Exceeded" error.
        # We could retry on a finer-grained error message later if necessary.
        403,
        500,  # Internal Server Error
        502,  # Bad Gateway
        503,  # Service Unavailable
    ]
    RETRIABLE_ERRORS = (httplib.HTTPException, httplib2.HttpLib2Error,
                        socket.error, ssl.SSLError)
    RETRIABLE_AUTH_ERRORS = (client.AccessTokenRefreshError, )

    def __init__(self, oauth2_credentials):
        """Initialize.

        Args:
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        self._service = self.InitResourceHandle(oauth2_credentials)

    @classmethod
    def InitResourceHandle(cls, oauth2_credentials):
        """Authenticate and initialize a Resource object.

        Authenticate http and create a Resource object with methods
        for interacting with the service.

        Args:
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.

        Returns:
            An apiclient.discovery.Resource object
        """
        http_auth = oauth2_credentials.authorize(httplib2.Http())
        return utils.RetryExceptionType(
                exception_types=cls.RETRIABLE_AUTH_ERRORS,
                max_retries=cls.RETRY_COUNT,
                functor=build,
                sleep_multiplier=cls.RETRY_SLEEP_MULTIPLIER,
                retry_backoff_factor=cls.RETRY_BACKOFF_FACTOR,
                serviceName=cls.API_NAME,
                version=cls.API_VERSION,
                http=http_auth)

    def _ShouldRetry(self, exception, retry_http_codes,
                     other_retriable_errors):
        """Check if exception is retriable.

        Args:
            exception: An instance of Exception.
            retry_http_codes: a list of integers, retriable HTTP codes of
                              HttpError
            other_retriable_errors: a tuple of error types to retry other than
                                    HttpError.

        Returns:
            Boolean, True if retriable, False otherwise.
        """
        if isinstance(exception, other_retriable_errors):
            return True

        if isinstance(exception, errors.HttpError):
            if exception.code in retry_http_codes:
                return True
            else:
                logger.debug("_ShouldRetry: Exception code %s not in %s: %s",
                             exception.code, retry_http_codes, str(exception))

        logger.debug(
            "_ShouldRetry: Exception %s is not one of %s: %s", type(exception),
            list(other_retriable_errors) + [errors.HttpError], str(exception))
        return False

    def _TranslateError(self, exception):
        """Translate the exception to a desired type.

        Args:
            exception: An instance of Exception.

        Returns:
            gerrors.HttpError will be translated to errors.HttpError.
            If the error code is errors.HTTP_NOT_FOUND_CODE, it will
            be translated to errors.ResourceNotFoundError.
            Unrecognized error type will not be translated and will
            be returned as is.
        """
        if isinstance(exception, gerrors.HttpError):
            exception = errors.HttpError.CreateFromHttpError(exception)
            if exception.code == errors.HTTP_NOT_FOUND_CODE:
                exception = errors.ResourceNotFoundError(exception.code,
                                                         str(exception))
        return exception

    def ExecuteOnce(self, api):
        """Execute an api and parse the errors.

        Args:
            api: An apiclient.http.HttpRequest, representing the api to execute.

        Returns:
            Execution result of the api.

        Raises:
            errors.ResourceNotFoundError: For 404 error.
            errors.HttpError: For other types of http error.
        """
        try:
            return api.execute()
        except gerrors.HttpError as e:
            raise self._TranslateError(e)

    def Execute(self,
                api,
                retry_http_codes=None,
                max_retry=None,
                sleep=None,
                backoff_factor=None,
                other_retriable_errors=None):
        """Execute an api with retry.

        Call ExecuteOnce and retry on http error with given codes.

        Args:
            api: An apiclient.http.HttpRequest, representing the api to execute:
            retry_http_codes: A list of http codes to retry.
            max_retry: See utils.Retry.
            sleep: See utils.Retry.
            backoff_factor: See utils.Retry.
            other_retriable_errors: A tuple of error types that should be retried
                                    other than errors.HttpError.

        Returns:
          Execution result of the api.

        Raises:
          See ExecuteOnce.
        """
        retry_http_codes = (self.RETRY_HTTP_CODES if retry_http_codes is None
                            else retry_http_codes)
        max_retry = (self.RETRY_COUNT if max_retry is None else max_retry)
        sleep = (self.RETRY_SLEEP_MULTIPLIER if sleep is None else sleep)
        backoff_factor = (self.RETRY_BACKOFF_FACTOR if backoff_factor is None
                          else backoff_factor)
        other_retriable_errors = (self.RETRIABLE_ERRORS
                                  if other_retriable_errors is None else
                                  other_retriable_errors)

        def _Handler(exc):
            """Check if |exc| is a retriable exception.

            Args:
                exc: An exception.

            Returns:
                True if exc is an errors.HttpError and code exists in |retry_http_codes|
                False otherwise.
            """
            if self._ShouldRetry(exc, retry_http_codes,
                                 other_retriable_errors):
                logger.debug("Will retry error: %s", str(exc))
                return True
            return False

        return utils.Retry(
             _Handler, max_retries=max_retry, functor=self.ExecuteOnce,
             sleep_multiplier=sleep, retry_backoff_factor=backoff_factor,
             api=api)

    def BatchExecuteOnce(self, requests):
        """Execute requests in a batch.

        Args:
            requests: A dictionary where key is request id and value
                      is an http request.

        Returns:
            results, a dictionary in the following format
            {request_id: (response, exception)}
            request_ids are those from requests; response
            is the http response for the request or None on error;
            exception is an instance of DriverError or None if no error.
        """
        results = {}

        def _CallBack(request_id, response, exception):
            results[request_id] = (response, self._TranslateError(exception))

        batch = apiclient.http.BatchHttpRequest()
        for request_id, request in requests.iteritems():
            batch.add(request=request,
                      callback=_CallBack,
                      request_id=request_id)
        batch.execute()
        return results

    def BatchExecute(self,
                     requests,
                     retry_http_codes=None,
                     max_retry=None,
                     sleep=None,
                     backoff_factor=None,
                     other_retriable_errors=None):
        """Batch execute multiple requests with retry.

        Call BatchExecuteOnce and retry on http error with given codes.

        Args:
            requests: A dictionary where key is request id picked by caller,
                      and value is a apiclient.http.HttpRequest.
            retry_http_codes: A list of http codes to retry.
            max_retry: See utils.Retry.
            sleep: See utils.Retry.
            backoff_factor: See utils.Retry.
            other_retriable_errors: A tuple of error types that should be retried
                                    other than errors.HttpError.

        Returns:
            results, a dictionary in the following format
            {request_id: (response, exception)}
            request_ids are those from requests; response
            is the http response for the request or None on error;
            exception is an instance of DriverError or None if no error.
        """
        executor = utils.BatchHttpRequestExecutor(
            self.BatchExecuteOnce,
            requests=requests,
            retry_http_codes=retry_http_codes or self.RETRY_HTTP_CODES,
            max_retry=max_retry or self.RETRY_COUNT,
            sleep=sleep or self.RETRY_SLEEP_MULTIPLIER,
            backoff_factor=backoff_factor or self.RETRY_BACKOFF_FACTOR,
            other_retriable_errors=other_retriable_errors or
            self.RETRIABLE_ERRORS)
        executor.Execute()
        return executor.GetResults()

    def ListWithMultiPages(self, api_resource, *args, **kwargs):
        """Call an api that list a type of resource.

        Multiple google services support listing a type of
        resource (e.g list gce instances, list storage objects).
        The querying pattern is similar --
        Step 1: execute the api and get a response object like,
        {
            "items": [..list of resource..],
            # The continuation token that can be used
            # to get the next page.
            "nextPageToken": "A String",
        }
        Step 2: execute the api again with the nextPageToken to
        retrieve more pages and get a response object.

        Step 3: Repeat Step 2 until no more page.

        This method encapsulates the generic logic of
        calling such listing api.

        Args:
            api_resource: An apiclient.discovery.Resource object
                used to create an http request for the listing api.
            *args: Arguments used to create the http request.
            **kwargs: Keyword based arguments to create the http
                      request.

        Returns:
            A list of items.
        """
        items = []
        next_page_token = None
        while True:
            api = api_resource(pageToken=next_page_token, *args, **kwargs)
            response = self.Execute(api)
            items.extend(response.get("items", []))
            next_page_token = response.get("nextPageToken")
            if not next_page_token:
                break
        return items

    @property
    def service(self):
        """Return self._service as a property."""
        return self._service