# -*- coding: utf-8 -*- #
# Copyright 2021 Google LLC. All Rights Reserved.
#
# 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.
"""A library used to interact with Operations objects."""
# TODO(b/73491568) Refactor to use api_lib.util.waiter


from googlecloudsdk.api_lib.functions.v1 import exceptions
from googlecloudsdk.core.console import progress_tracker as console_progress_tracker
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import retry

MAX_WAIT_MS = 1820000
WAIT_CEILING_MS = 2000
SLEEP_MS = 1000


def OperationErrorToString(error):
  """Returns a human readable string representation from the operation.

  Args:
    error: A string representing the raw json of the operation error.

  Returns:
    A human readable string representation of the error.
  """
  return 'OperationError: code={0}, message={1}'.format(
      error.code, encoding.Decode(error.message)
  )


# TODO(b/130604453): Remove try_set_invoker option.
def _GetOperationStatus(
    client,
    get_request,
    progress_tracker=None,
    try_set_invoker=None,
    on_every_poll=None,
):
  """Helper function for getting the status of an operation.

  Args:
    client: The client used to make requests.
    get_request: A GetOperationRequest message.
    progress_tracker: progress_tracker.ProgressTracker, A reference for the
      progress tracker to tick, in case this function is used in a Retryer.
    try_set_invoker: function to try setting invoker, see above TODO.
    on_every_poll: list of functions to execute every time we poll. Functions
      should take in Operation as an argument.

  Returns:
    True if the operation succeeded without error.
    False if the operation is not yet done.

  Raises:
    FunctionsError: If the operation is finished with error.
  """
  if try_set_invoker:
    try_set_invoker()
  if progress_tracker:
    progress_tracker.Tick()
  op = client.operations.Get(get_request)
  if op.error:
    raise exceptions.FunctionsError(OperationErrorToString(op.error))
  if on_every_poll:
    for function in on_every_poll:
      function(op)
  return op.done


# TODO(b/139026575): Remove try_set_invoker option.
def _WaitForOperation(
    client, get_request, message, try_set_invoker=None, on_every_poll=None
):
  """Wait for an operation to complete.

  No operation is done instantly. Wait for it to finish following this logic:
  * we wait 1s (jitter is also 1s)
  * we query service
  * if the operation is not finished we loop to first point
  * wait limit is 1820s - if we get to that point it means something is wrong
        and we can throw an exception

  Args:
    client:  The client used to make requests.
    get_request: A GetOperationRequest message.
    message: str, The string to print while polling.
    try_set_invoker: function to try setting invoker, see above TODO.
    on_every_poll: list of functions to execute every time we poll. Functions
      should take in Operation as an argument.

  Returns:
    True if the operation succeeded without error.

  Raises:
    FunctionsError: If the operation takes more than 1820s.
  """

  with console_progress_tracker.ProgressTracker(message, autotick=False) as pt:
    # This is actually linear retryer.
    retryer = retry.Retryer(
        exponential_sleep_multiplier=1,
        max_wait_ms=MAX_WAIT_MS,
        wait_ceiling_ms=WAIT_CEILING_MS,
    )
    try:
      retryer.RetryOnResult(
          _GetOperationStatus,
          [client, get_request],
          {
              'progress_tracker': pt,
              'try_set_invoker': try_set_invoker,
              'on_every_poll': on_every_poll,
          },
          should_retry_if=lambda done, _: not done,
          sleep_ms=SLEEP_MS,
      )
    except retry.WaitException:
      raise exceptions.FunctionsError(
          'Operation {0} is taking too long'.format(get_request.name)
      )


def Wait(
    operation,
    messages,
    client,
    notice=None,
    try_set_invoker=None,
    on_every_poll=None,
):
  """Initialize waiting for operation to finish.

  Generate get request based on the operation and wait for an operation
  to complete.

  Args:
    operation: The operation which we are waiting for.
    messages: GCF messages module.
    client: GCF client module.
    notice: str, displayed when waiting for the operation to finish.
    try_set_invoker: function to try setting invoker, see above TODO.
    on_every_poll: list of functions to execute every time we poll. Functions
      should take in Operation as an argument.

  Raises:
    FunctionsError: If the operation takes more than 620s.
  """
  if notice is None:
    notice = 'Waiting for operation to finish'
  request = messages.CloudfunctionsOperationsGetRequest()
  request.name = operation.name
  _WaitForOperation(client, request, notice, try_set_invoker, on_every_poll)
