# -*- coding: utf-8 -*- #
# Copyright 2017 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.

"""Utility methods to aid in interacting with a GCS results bucket."""

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import os

from apitools.base.py import exceptions as apitools_exceptions
from apitools.base.py import transfer

from googlecloudsdk.api_lib.firebase.test import util
from googlecloudsdk.api_lib.util import apis as core_apis
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import resources


GCS_PREFIX = 'gs://'
HTTP_FORBIDDEN = 403
HTTP_NOT_FOUND = 404


class ResultsBucketOps(object):
  """A utility class to encapsulate operations on the results bucket."""

  def __init__(self, project, bucket_name, unique_obj_name,
               tr_client, tr_messages, storage_client):
    """Construct a ResultsBucketOps object to be used with a single matrix run.

    Args:
      project: string containing the Google Cloud Platform project id.
      bucket_name: string with the user-supplied name of a GCS bucket, or None.
      unique_obj_name: a unique-per-matrix GCS object name which will prefix all
        raw test result files within the supplied bucket_name.
      tr_client: ToolResults API client library generated by Apitools.
      tr_messages: ToolResults API messages library generated by Apitools.
      storage_client: Cloud Storage API client library generated by Apitools.

    Attributes:
      gcs_results_root: string containing the root path for the test results in
        'gs://{bucket}/{timestamp-suffix}' format.
    """
    self._project = project
    self._storage_client = storage_client
    self._storage_messages = core_apis.GetMessagesModule('storage', 'v1')
    self._gcs_object_name = unique_obj_name

    # If the user supplied a results bucket, make sure it exists. Otherwise,
    # call the SettingsService to get the project's existing default bucket.
    if bucket_name:
      self.EnsureBucketExists(bucket_name)
    else:
      bucket_name = self._GetDefaultBucket(tr_client, tr_messages)

    bucket_ref = resources.REGISTRY.Parse(
        bucket_name, collection='storage.buckets')
    self._results_bucket = bucket_ref.bucket

    self._gcs_results_url = (
        'https://console.developers.google.com/storage/browser/{b}/{t}/'
        .format(b=bucket_name, t=self._gcs_object_name))
    self.gcs_results_root = ('gs://{b}/{t}/'
                             .format(b=bucket_name, t=self._gcs_object_name))
    log.info('Raw results root path is: [{0}]'.format(self.gcs_results_root))

  def _GetDefaultBucket(self, tr_client, tr_messages):
    """Fetch the project's default GCS bucket name for storing tool results."""
    request = tr_messages.ToolresultsProjectsInitializeSettingsRequest(
        projectId=self._project)
    try:
      response = tr_client.projects.InitializeSettings(request)
      return response.defaultBucket
    except apitools_exceptions.HttpError as error:
      code, err_msg = util.GetErrorCodeAndMessage(error)
      if code == HTTP_FORBIDDEN:
        msg = ('Permission denied while fetching the default results bucket '
               '(Error {0}: {1}). Is billing enabled for project: [{2}]?'
               .format(code, err_msg, self._project))
      else:
        msg = ('Http error while trying to fetch the default results bucket:\n'
               'ResponseError {0}: {1}'
               .format(code, err_msg))
      raise exceptions.HttpException(msg)

  def EnsureBucketExists(self, bucket_name):
    """Create a GCS bucket if it doesn't already exist.

    Args:
      bucket_name: the name of the GCS bucket to create if it doesn't exist.

    Raises:
      BadFileException if the bucket name is malformed, the user does not
        have access rights to the bucket, or the bucket can't be created.
    """
    get_req = self._storage_messages.StorageBucketsGetRequest(
        bucket=bucket_name)
    try:
      self._storage_client.buckets.Get(get_req)
      return  # The bucket exists and the user can access it.
    except apitools_exceptions.HttpError as err:
      code, err_msg = util.GetErrorCodeAndMessage(err)
      if code != HTTP_NOT_FOUND:
        raise exceptions.BadFileException(
            'Could not access bucket [{b}]. Response error {c}: {e}. '
            'Please supply a valid bucket name or use the default bucket '
            'provided by Firebase Test Lab.'
            .format(b=bucket_name, c=code, e=err_msg))

    # The bucket does not exist in any project, so create it in user's project.
    log.status.Print('Creating results bucket [{g}{b}] in project [{p}].'
                     .format(g=GCS_PREFIX, b=bucket_name, p=self._project))

    bucket_req = self._storage_messages.StorageBucketsInsertRequest
    acl = bucket_req.PredefinedAclValueValuesEnum.projectPrivate
    objacl = bucket_req.PredefinedDefaultObjectAclValueValuesEnum.projectPrivate

    insert_req = self._storage_messages.StorageBucketsInsertRequest(
        bucket=self._storage_messages.Bucket(name=bucket_name),
        predefinedAcl=acl,
        predefinedDefaultObjectAcl=objacl,
        project=self._project)
    try:
      self._storage_client.buckets.Insert(insert_req)
      return
    except apitools_exceptions.HttpError as err:

      code, err_msg = util.GetErrorCodeAndMessage(err)
      if code == HTTP_FORBIDDEN:
        msg = ('Permission denied while creating bucket [{b}]. '
               'Is billing enabled for project: [{p}]?'
               .format(b=bucket_name, p=self._project))
      else:
        msg = ('Failed to create bucket [{b}] {e}'
               .format(b=bucket_name, e=util.GetError(err)))
      raise exceptions.BadFileException(msg)

  def UploadFileToGcs(self, path, mimetype=None, destination_object=None):
    """Upload a file to the GCS results bucket using the storage API.

    Args:
      path: str, the absolute or relative path of the file to upload. File may
        be in located in GCS or the local filesystem.
      mimetype: str, the MIME type (aka Content-Type) that should be applied to
        files being copied from a non-GCS source to GCS. MIME types for GCS->GCS
        file uploads are not modified.
      destination_object: str, the destination object path in GCS to upload to,
        if it's different than the base name of the path argument.

    Raises:
      BadFileException if the file upload is not successful.
    """
    log.status.Print('Uploading [{f}] to Firebase Test Lab...'.format(f=path))
    try:
      if path.startswith(GCS_PREFIX):
        # Perform a GCS object to GCS object copy
        file_bucket, file_obj = _SplitBucketAndObject(path)
        copy_req = self._storage_messages.StorageObjectsCopyRequest(
            sourceBucket=file_bucket,
            sourceObject=file_obj,
            destinationBucket=self._results_bucket,
            destinationObject='{obj}/{name}'.format(
                obj=self._gcs_object_name,
                name=destination_object or os.path.basename(file_obj)))
        self._storage_client.objects.Copy(copy_req)
      else:
        # Perform a GCS insert of a file which is not in GCS
        try:
          file_size = os.path.getsize(path)
        except os.error:
          raise exceptions.BadFileException('[{0}] not found or not accessible'
                                            .format(path))
        src_obj = self._storage_messages.Object(size=file_size)
        try:
          upload = transfer.Upload.FromFile(path, mime_type=mimetype)
        except apitools_exceptions.InvalidUserInputError:
          upload = transfer.Upload.FromFile(
              path, mime_type='application/octet-stream')

        insert_req = self._storage_messages.StorageObjectsInsertRequest(
            bucket=self._results_bucket,
            name='{obj}/{name}'.format(
                obj=self._gcs_object_name,
                name=destination_object or os.path.basename(path)),
            object=src_obj)
        response = self._storage_client.objects.Insert(insert_req,
                                                       upload=upload)
        if response.size != file_size:
          raise exceptions.BadFileException(
              'Cloud storage upload failure: Insert response.size={0} bytes '
              'but [{1}] contains {2} bytes.\nInsert response: {3}'
              .format(response.size, path, file_size, repr(response)))
    except apitools_exceptions.HttpError as err:
      raise exceptions.BadFileException(
          'Could not copy [{f}] to [{gcs}] {e}.'
          .format(f=path, gcs=self.gcs_results_root, e=util.GetError(err)))

  def LogGcsResultsUrl(self):
    log.status.Print('Raw results will be stored in your GCS bucket at [{0}]\n'
                     .format(self._gcs_results_url))


def _SplitBucketAndObject(gcs_path):
  """Split a GCS path into bucket & object tokens, or raise BadFileException."""
  tokens = gcs_path[len(GCS_PREFIX):].strip('/').split('/', 1)
  if len(tokens) != 2:
    raise exceptions.BadFileException(
        '[{0}] is not a valid Google Cloud Storage path'.format(gcs_path))
  return tokens
