# Copyright (C) 2020 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.


"""Utility functions for logstorage."""
from __future__ import print_function

import logging
import time
import uuid

from atest import atest_utils
from atest import constants
from atest.logstorage import atest_gcp_utils
from atest.metrics import metrics_base
from googleapiclient.discovery import build
import httplib2
from oauth2client import client as oauth2_client

UPLOAD_REQUESTED_FILE_NAME = 'UPLOAD_REQUESTED'


def is_credential_available() -> bool:
  """Checks whether the credential needed for log upload is available."""
  return constants.CREDENTIAL_FILE_NAME and constants.TOKEN_FILE_PATH


def is_upload_enabled(args: dict[str, str]) -> bool:
  """Determines whether log upload is enabled."""
  if not is_credential_available() or not constants.GTF_TARGETS:
    return False

  config_folder_path = atest_utils.get_config_folder()
  config_folder_path.mkdir(parents=True, exist_ok=True)
  upload_requested_file = config_folder_path.joinpath(
      UPLOAD_REQUESTED_FILE_NAME
  )

  is_request_upload = args.get(constants.REQUEST_UPLOAD_RESULT)
  is_disable_upload = args.get(constants.DISABLE_UPLOAD_RESULT)
  is_previously_requested = upload_requested_file.exists()

  # Note: is_request_upload and is_disable_upload are from mutually exclusive
  # args so they won't be True simutaniously.
  if not is_disable_upload and is_previously_requested:  # Previously enabled
    atest_utils.colorful_print(
        'AnTS result uploading is enabled. (To disable, use'
        ' --disable-upload-result flag)',
        constants.GREEN,
    )
    return True

  if is_request_upload and not is_previously_requested:  # First time enable
    atest_utils.colorful_print(
        'AnTS result uploading is switched on and will apply to the current and'
        ' future TradeFed test runs. To disable it, run a test with the'
        ' --disable-upload-result flag.',
        constants.GREEN,
    )
    upload_requested_file.touch()
    return True

  if is_disable_upload and is_previously_requested:  # First time disable
    atest_utils.colorful_print(
        'AnTS result uploading is switched off and will apply to the current'
        ' and future TradeFed test runs. To re-enable it, run a test with the'
        ' --request-upload-result flag.',
        constants.GREEN,
    )
    upload_requested_file.unlink()
    config_folder_path.joinpath(constants.CREDENTIAL_FILE_NAME).unlink(
        missing_ok=True
    )
    return False

  return False


def do_upload_flow(
    extra_args: dict[str, str], invocation_properties: dict[str, str] = None
) -> tuple:
  """Run upload flow.

  Asking user's decision and do the related steps.

  Args:
      extra_args: Dict of extra args to add to test run.
      invocation_properties: Additional invocation properties to write into the
        invocation.

  Return:
      A tuple of credential object and invocation information dict.
  """
  invocation_properties = invocation_properties or {}
  return atest_gcp_utils.do_upload_flow(
      extra_args, lambda cred: BuildClient(cred), invocation_properties
  )


class BuildClient:
  """Build api helper class."""

  def __init__(
      self,
      creds,
      api_version=constants.STORAGE_API_VERSION,
      url=constants.DISCOVERY_SERVICE,
  ):
    """Init BuildClient class.

    Args:
        creds: An oauth2client.OAuth2Credentials instance.
    """
    http_auth = creds.authorize(httplib2.Http())
    self.client = build(
        serviceName=constants.STORAGE_SERVICE_NAME,
        version=api_version,
        cache_discovery=False,
        http=http_auth,
        discoveryServiceUrl=url,
    )

  def list_branch(self):
    """List all branch."""
    return self.client.branch().list(maxResults=10000).execute()

  def list_target(self, branch):
    """List all target in the branch."""
    return self.client.target().list(branch=branch, maxResults=10000).execute()

  def get_branch(self, branch):
    """Get BuildInfo for specific branch.

    Args:
        branch: A string of branch name to query.
    """
    query_branch = ''
    try:
      query_branch = self.client.branch().get(resourceId=branch).execute()
    # pylint: disable=broad-except
    except Exception:
      return ''
    return query_branch

  def insert_local_build(self, external_id, target, branch):
    """Insert a build record.

    Args:
        external_id: unique id of build record.
        target: build target.
        branch: build branch.

    Returns:
        A build record object.
    """
    body = {
        'buildId': '',
        'externalId': external_id,
        'branch': branch,
        'target': {'name': target, 'target': target},
        'buildAttemptStatus': 'complete',
    }
    return self.client.build().insert(buildType='local', body=body).execute()

  def insert_build_attempts(self, build_record):
    """Insert a build attempt record.

    Args:
        build_record: build record.

    Returns:
        A build attempt object.
    """
    build_attempt = {'id': 0, 'status': 'complete', 'successful': True}
    return (
        self.client.buildattempt()
        .insert(
            buildId=build_record['buildId'],
            target=build_record['target']['name'],
            body=build_attempt,
        )
        .execute()
    )

  def insert_invocation(
      self, build_record: dict[str, str], invocation_properties: dict[str, str]
  ):
    """Insert a build invocation record.

    Args:
        build_record: build record.
        invocation_properties: Additional invocation properties to write into
          the invocation.

    Returns:
        A build invocation object.
    """
    sponge_invocation_id = str(uuid.uuid4())
    user_email = metrics_base.get_user_email()
    invocation = {
        'primaryBuild': {
            'buildId': build_record['buildId'],
            'buildTarget': build_record['target']['name'],
            'branch': build_record['branch'],
        },
        'schedulerState': 'running',
        'runner': 'atest',
        'scheduler': 'atest',
        'users': [user_email],
        'properties': [
            {
                'name': 'sponge_invocation_id',
                'value': sponge_invocation_id,
            },
            {
                'name': 'test_uri',
                'value': f'{constants.STORAGE2_TEST_URI}{sponge_invocation_id}',
            },
        ] + [
            {'name': key, 'value': value}
            for key, value in invocation_properties.items()
        ],
    }
    return self.client.invocation().insert(body=invocation).execute()

  def update_invocation(self, invocation):
    """Insert a build invocation record.

    Args:
        invocation: invocation record.

    Returns:
        A invocation object.
    """
    # Because invocation revision will be update by TF, we need to fetch
    # latest invocation revision to update status correctly.
    count = 0
    invocations = None
    while count < 5:
      invocations = (
          self.client.invocation()
          .list(invocationId=invocation['invocationId'], maxResults=10)
          .execute()
          .get('invocations', [])
      )
      if invocations:
        break
      time.sleep(0.5)
      count = count + 1
    if invocations:
      latest_revision = invocations[-1].get('revision', '')
      if latest_revision:
        logging.debug(
            'Get latest_revision:%s from invocations:%s',
            latest_revision,
            invocations,
        )
        invocation['revision'] = latest_revision
    return (
        self.client.invocation()
        .update(resourceId=invocation['invocationId'], body=invocation)
        .execute()
    )

  def insert_work_unit(self, invocation_record):
    """Insert a workunit record.

    Args:
        invocation_record: invocation record.

    Returns:
        the workunit object.
    """
    workunit = {'invocationId': invocation_record['invocationId']}
    return self.client.workunit().insert(body=workunit).execute()
