# -*- 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.

"""A yaml to calliope command translator.

Calliope allows you to register a hook that converts a yaml command spec into
a calliope command class. The Translator class in this module implements that
interface and provides generators for a yaml command spec. The schema for the
spec can be found in yaml_command_schema.yaml.
"""

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

import json
import sys

from apitools.base.protorpclite import messages as apitools_messages
from apitools.base.py import encoding
from apitools.base.py import exceptions as apitools_exceptions
from apitools.base.py.exceptions import HttpBadRequestError
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import command_loading
from googlecloudsdk.command_lib.iam import iam_util
from googlecloudsdk.command_lib.util import completers
from googlecloudsdk.command_lib.util.apis import arg_marshalling
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import registry
from googlecloudsdk.command_lib.util.apis import update
from googlecloudsdk.command_lib.util.apis import yaml_command_schema
from googlecloudsdk.command_lib.util.args import labels_util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.resource import resource_transform
from googlecloudsdk.core.util import files

import six


class Translator(command_loading.YamlCommandTranslator):
  """Class that implements the calliope translator interface."""

  def Translate(self, path, command_data):
    spec = yaml_command_schema.CommandData(path[-1], command_data)
    c = CommandBuilder(spec, path)
    return c.Generate()


class DeclarativeIamRolesCompleter(completers.ListCommandCompleter):
  """An IAM role completer for a resource argument.

  The Complete() method override bypasses the completion cache.

  Attributes:
    _get_resource_ref: DeclarativeArgumentGenerator.GetRequestResourceRef method
      to parse the resource ref.
  """

  def __init__(self, get_resource_ref, **kwargs):
    super(DeclarativeIamRolesCompleter, self).__init__(**kwargs)
    self._get_resource_ref = get_resource_ref

  def GetListCommand(self, parameter_info):
    resource_ref = self._get_resource_ref(parameter_info.parsed_args)
    resource_uri = resource_ref.SelfLink()
    return [
        'iam', 'list-grantable-roles', '--quiet', '--flatten=name',
        '--format=disable', resource_uri
    ]

  def Complete(self, prefix, parameter_info):
    """Bypasses the cache and returns completions matching prefix."""
    command = self.GetListCommand(parameter_info)
    items = self.GetAllItems(command, parameter_info)
    return [
        item for item in items or []
        if item is not None and item.startswith(prefix)
    ]


class CommandBuilder(object):
  """Generates calliope commands based on the yaml spec."""

  IGNORED_FLAGS = {'project'}

  def __init__(self, spec, path):
    self.spec = spec
    self.path = path
    self.ConfigureCommand()

  def ConfigureCommand(self):
    """Allows command to be reconfigured if needed."""
    self.method = registry.GetMethod(self.spec.request.collection,
                                     self.spec.request.method,
                                     self.spec.request.api_version)
    resource_arg = self.spec.arguments.resource
    self.arg_generator = arg_marshalling.DeclarativeArgumentGenerator(
        self.method, self.spec.arguments.params, resource_arg)
    self.display_resource_type = self.spec.request.display_resource_type
    if (not self.display_resource_type and resource_arg and
        not resource_arg.is_parent_resource):
      self.display_resource_type = resource_arg.name if resource_arg else None

  def Generate(self):
    """Generates a calliope command from the yaml spec.

    Raises:
      ValueError: If we don't know how to generate the given command type (this
        is not actually possible right now due to the enum).

    Returns:
      calliope.base.Command, The command that implements the spec.
    """
    if self.spec.command_type == yaml_command_schema.CommandType.DESCRIBE:
      command = self._GenerateDescribeCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.LIST:
      command = self._GenerateListCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.DELETE:
      command = self._GenerateDeleteCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.CREATE:
      command = self._GenerateCreateCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.WAIT:
      command = self._GenerateWaitCommand()
    elif (self.spec.command_type ==
          yaml_command_schema.CommandType.GET_IAM_POLICY):
      command = self._GenerateGetIamPolicyCommand()
    elif (self.spec.command_type ==
          yaml_command_schema.CommandType.SET_IAM_POLICY):
      command = self._GenerateSetIamPolicyCommand()
    elif (self.spec.command_type ==
          yaml_command_schema.CommandType.ADD_IAM_POLICY_BINDING):
      command = self._GenerateAddIamPolicyBindingCommand()
    elif (self.spec.command_type ==
          yaml_command_schema.CommandType.REMOVE_IAM_POLICY_BINDING):
      command = self._GenerateRemoveIamPolicyBindingCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.UPDATE:
      command = self._GenerateUpdateCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.IMPORT:
      command = self._GenerateImportCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.EXPORT:
      command = self._GenerateExportCommand()
    elif self.spec.command_type == yaml_command_schema.CommandType.GENERIC:
      command = self._GenerateGenericCommand()
    else:
      raise ValueError('Command [{}] unknown command type [{}].'.format(
          ' '.join(self.path), self.spec.command_type))
    self._ConfigureGlobalAttributes(command)
    return command

  def _GenerateDescribeCommand(self):
    """Generates a Describe command.

    A describe command has a single resource argument and an API method to call
    to get the resource. The result is returned using the default output format.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.DescribeCommand):

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)

      def Run(self_, args):
        unused_ref, response = self._CommonRun(args)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateListCommand(self):
    """Generates a List command.

    A list command operates on a single resource and has flags for the parent
    collection of that resource. Because it extends the calliope base List
    command, it gets flags for things like limit, filter, and page size. A
    list command should register a table output format to display the result.
    If arguments.resource.response_id_field is specified, a --uri flag will also
    be enabled.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.ListCommand):
    # pylint: disable=missing-docstring

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        # Remove the URI flag if we don't know how to generate URIs for this
        # resource.
        if not self.spec.response.id_field:
          base.URI_FLAG.RemoveFromParser(parser)

      def Run(self_, args):
        self._RegisterURIFunc(args)
        unused_ref, response = self._CommonRun(args)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateDeleteCommand(self):
    """Generates a Delete command.

    A delete command has a single resource argument and an API to call to
    perform the delete. If the async section is given in the spec, an --async
    flag is added and polling is automatically done on the response. For APIs
    that adhere to standards, no further configuration is necessary. If the API
    uses custom operations, you may need to provide extra configuration to
    describe how to poll the operation.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.DeleteCommand):
    # pylint: disable=missing-docstring

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        if self.spec.async_:
          base.ASYNC_FLAG.AddToParser(parser)

      def Run(self_, args):
        ref, response = self._CommonRun(args)
        if self.spec.async_:
          response = self._HandleAsync(
              args,
              ref,
              response,
              request_string='Delete request issued for: [{{{}}}]'
              .format(yaml_command_schema.NAME_FORMAT_KEY),
              extract_resource_result=False)
          if args.async_:
            return self._HandleResponse(response, args)

        response = self._HandleResponse(response, args)
        log.DeletedResource(self._GetDisplayName(ref, args),
                            kind=self.display_resource_type)
        return response

    return Command

  def _GenerateCreateCommand(self):
    """Generates a Create command.

    A create command has a single resource argument and an API to call to
    perform the creation. If the async section is given in the spec, an --async
    flag is added and polling is automatically done on the response. For APIs
    that adhere to standards, no further configuration is necessary. If the API
    uses custom operations, you may need to provide extra configuration to
    describe how to poll the operation.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.CreateCommand):
    # pylint: disable=missing-docstring

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        if self.spec.async_:
          base.ASYNC_FLAG.AddToParser(parser)
        if self.spec.arguments.labels:
          labels_util.AddCreateLabelsFlags(parser)

      def Run(self_, args):
        ref, response = self._CommonRun(args)
        is_parent_resource = (self.spec.arguments.resource and
                              self.spec.arguments.resource.is_parent_resource)
        if self.spec.async_:
          if ref is not None and not is_parent_resource:
            request_string = 'Create request issued for: [{{{}}}]'.format(
                yaml_command_schema.NAME_FORMAT_KEY)
          else:
            request_string = 'Create request issued'
          response = self._HandleAsync(
              args, ref, response,
              request_string=request_string)
          if args.async_:
            return self._HandleResponse(response, args)

        if is_parent_resource:
          # Data on responses from operation polling is stored in
          # additionalProperties, so convert to dict for consistent behavior.
          response_obj = encoding.MessageToDict(response)
          # If the response is an operation that has a 'response' property that
          # has a name, use that. Otherwise, use the 'name' property.
          full_name = response_obj.get('response', {}).get('name')
          if not full_name:
            full_name = response_obj.get('name')
          resource_name = resource_transform.TransformBaseName(full_name)
        else:
          resource_name = self._GetDisplayName(ref, args)
        log.CreatedResource(resource_name, kind=self.display_resource_type)
        response = self._HandleResponse(response, args)
        return response

    return Command

  def _GenerateWaitCommand(self):
    """Generates a wait command for polling operations.

    A wait command takes an operation reference and polls the status until it
    is finished or errors out. This follows the exact same spec as in other
    async commands except the primary operation (create, delete, etc) has
    already been done. For APIs that adhere to standards, no further async
    configuration is necessary. If the API uses custom operations, you may need
    to provide extra configuration to describe how to poll the operation.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.Command):
    # pylint: disable=missing-docstring

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)

      def Run(self_, args):
        ref = self.arg_generator.GetRequestResourceRef(args)
        response = self._WaitForOperation(
            ref, resource_ref=None, extract_resource_result=False,
            args=args)
        response = self._HandleResponse(response, args)
        return response

    return Command

  @property
  def _add_condition(self):
    return self.spec.iam and self.spec.iam.enable_condition

  def _GenerateGetIamPolicyCommand(self):
    """Generates a get-iam-policy command.

    A get-iam-policy command has a single resource argument and an API method
    to call to get the resource. The result is returned using the default
    output format.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.ListCommand):
      """Get IAM policy command closure."""

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        base.URI_FLAG.RemoveFromParser(parser)

      def Run(self_, args):
        if self.spec.iam and self.spec.iam.policy_version:
          self.spec.request.static_fields[
              self.spec.iam
              .get_iam_policy_version_path] = self.spec.iam.policy_version

        _, response = self._CommonRun(args)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateSetIamPolicyCommand(self):
    """Generates a set-iam-policy command.

    A set-iam-policy command takes a resource argument, a policy to set on that
    resource, and an API method to call to set the policy on the resource. The
    result is returned using the default output format.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.Command):
      """Set IAM policy command closure."""

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        iam_util.AddArgForPolicyFile(parser)
        base.URI_FLAG.RemoveFromParser(parser)

      def Run(self_, args):
        """Called when command is executed."""
        # Default Policy message and set IAM request message field names
        policy_type_name = 'Policy'
        policy_request_path = 'setIamPolicyRequest'

        # Use Policy message and set IAM request field name overrides for API's
        # with non-standard naming (if provided)
        if self.spec.iam:
          if 'policy' in self.spec.iam.message_type_overrides:
            policy_type_name = (self.spec.iam
                                .message_type_overrides['policy'] or
                                policy_type_name)
          policy_request_path = (self.spec.iam.set_iam_policy_request_path or
                                 policy_request_path)

        policy_field_path = policy_request_path + '.policy'
        policy_type = self.method.GetMessageByName(policy_type_name)
        if not policy_type:
          raise ValueError('Policy type [{}] not found.'.format(
              policy_type_name))
        policy, update_mask = iam_util.ParsePolicyFileWithUpdateMask(
            args.policy_file, policy_type)

        # override policy version
        if self.spec.iam and self.spec.iam.policy_version:
          policy.version = self.spec.iam.policy_version

        self.spec.request.static_fields[policy_field_path] = policy
        self._SetPolicyUpdateMask(update_mask)
        try:
          ref, response = self._CommonRun(args)
        except HttpBadRequestError as ex:
          log.err.Print(
              'ERROR: Policy modification failed. For bindings with conditions'
              ', run "gcloud alpha iam policies lint-condition" to identify '
              'issues in conditions.'
          )
          raise ex

        iam_util.LogSetIamPolicy(ref.Name(), self.display_resource_type)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateDeclarativeIamRolesCompleter(self):
    """Generate a IAM role completer."""

    get_resource_ref = self.arg_generator.GetRequestResourceRef

    class Completer(DeclarativeIamRolesCompleter):

      def __init__(self, **kwargs):
        super(Completer, self).__init__(
            get_resource_ref=get_resource_ref, **kwargs)

    return Completer

  def _GenerateAddIamPolicyBindingCommand(self):
    """Generates an add-iam-policy-binding command.

    An add-iam-policy-binding command adds a binding to a IAM policy. A
    binding consists of a member, a role to define the role of the member, and
    an optional condition to define in what condition the binding is valid.
    Two API methods are called to get and set the policy on the resource.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.Command):
      """Add IAM policy binding command closure."""

      @staticmethod
      def Args(parser):
        iam_util.AddArgsForAddIamPolicyBinding(
            parser,
            role_completer=self._GenerateDeclarativeIamRolesCompleter(),
            add_condition=self._add_condition)
        self._CommonArgs(parser)
        base.URI_FLAG.RemoveFromParser(parser)

      def Run(self_, args):
        """Called when command is executed."""
        # Use Policy message and set IAM request field name overrides for API's
        # with non-standard naming (if provided)
        policy_request_path = 'setIamPolicyRequest'
        if self.spec.iam:
          policy_request_path = (
              self.spec.iam.set_iam_policy_request_path or policy_request_path)
        policy_field_path = policy_request_path + '.policy'

        policy = self._GetModifiedIamPolicyAddIamBinding(
            args, add_condition=self._add_condition)

        # override policy version
        if self.spec.iam and self.spec.iam.policy_version:
          policy.version = self.spec.iam.policy_version

        self.spec.request.static_fields[policy_field_path] = policy

        try:
          ref, response = self._CommonRun(args)
        except HttpBadRequestError as ex:
          log.err.Print(
              'ERROR: Policy modification failed. For a binding with condition'
              ', run "gcloud alpha iam policies lint-condition" to identify '
              'issues in condition.'
          )
          raise ex

        iam_util.LogSetIamPolicy(ref.Name(), self.display_resource_type)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateRemoveIamPolicyBindingCommand(self):
    """Generates a remove-iam-policy-binding command.

    A remove-iam-policy-binding command removes a binding from a IAM policy. A
    binding consists of a member, a role to define the role of the member, and
    an optional condition to define in what condition the binding is valid.
    Two API methods are called to get and set the policy on the resource.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.Command):
      """Remove IAM policy binding command closure."""

      @staticmethod
      def Args(parser):
        iam_util.AddArgsForRemoveIamPolicyBinding(
            parser,
            role_completer=self._GenerateDeclarativeIamRolesCompleter(),
            add_condition=self._add_condition)
        self._CommonArgs(parser)
        base.URI_FLAG.RemoveFromParser(parser)

      def Run(self_, args):
        """Called when command is executed."""
        # Use Policy message and set IAM request field name overrides for API's
        # with non-standard naming (if provided)
        policy_request_path = 'setIamPolicyRequest'
        if self.spec.iam:
          policy_request_path = (
              self.spec.iam.set_iam_policy_request_path or policy_request_path)
        policy_field_path = policy_request_path + '.policy'

        policy = self._GetModifiedIamPolicyRemoveIamBinding(
            args, add_condition=self._add_condition)

        # override policy version
        if self.spec.iam and self.spec.iam.policy_version:
          policy.version = self.spec.iam.policy_version

        self.spec.request.static_fields[policy_field_path] = policy

        ref, response = self._CommonRun(args)
        iam_util.LogSetIamPolicy(ref.Name(), self.display_resource_type)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateGenericCommand(self):
    """Generates a generic command.

    A generic command has a resource argument, additional fields, and calls an
    API method. It supports async if the async configuration is given. Any
    fields is message_params will be generated as arguments and inserted into
    the request message.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.Command):
      # pylint: disable=missing-docstring

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        if self.spec.async_:
          base.ASYNC_FLAG.AddToParser(parser)

      def Run(self_, args):
        ref, response = self._CommonRun(args)
        if self.spec.async_:
          request_string = None
          if ref:
            request_string = 'Request issued for: [{{{}}}]'.format(
                yaml_command_schema.NAME_FORMAT_KEY)
          response = self._HandleAsync(
              args, ref, response, request_string=request_string)
        return self._HandleResponse(response, args)

    return Command

  def _GenerateImportCommand(self):
    """Generates an export command.

    An export command has a single resource argument and an API method to call
    to get the resource. The result is exported to a local yaml file provided
    by the `--destination` flag, or to stdout if nothing is provided.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """
    # Lazy import to prevent drag on startup time.
    from googlecloudsdk.command_lib.export import util as export_util  # pylint:disable=g-import-not-at-top

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.ImportCommand):
      """Export command enclosure."""

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        if self.spec.async_:
          base.ASYNC_FLAG.AddToParser(parser)
        parser.add_argument(
            '--source',
            help="""
            Path to a YAML file containing the configuration export data. The
            YAML file must not contain any output-only fields. Alternatively, you
            may omit this flag to read from standard input. A schema describing
            the export/import format can be found in:
            $CLOUDSDKROOT/lib/googlecloudsdk/schemas/...
          """)

      def Run(self_, args):
        # Determine message to parse resource into from yaml
        message_type = self.method.GetRequestType()
        request_field = self.method.request_field
        resource_message_class = message_type.field_by_name(request_field).type

        # Set up information for export utility.
        data = console_io.ReadFromFileOrStdin(args.source or '-', binary=False)
        schema_path = export_util.GetSchemaPath(self.method.collection.api_name,
                                                self.spec.request.api_version,
                                                resource_message_class.__name__)
        # Import resource from yaml.
        imported_resource = export_util.Import(
            message_type=resource_message_class,
            stream=data,
            schema_path=schema_path)

        # If any special configuration has been made for the import command...
        existing_resource = None
        if self.spec.import_:
          abort_if_equivalent = self.spec.import_.abort_if_equivalent
          create_if_not_exists = self.spec.import_.create_if_not_exists

          # Try to get the existing resource from the service.
          try:
            existing_resource = self._GetExistingResource(args)
          except apitools_exceptions.HttpError as error:
            # Raise error if command is configured to not create a new resource
            # or if error other than "Does Not Exist" occurs.
            if error.status_code != 404 or not create_if_not_exists:
              raise error
            else:
              # Configure command to use fallback create request configuration.
              self.spec.request = self.spec.import_.create_request

              # Configure command to use fallback create async configuration.
              if self.spec.import_.no_create_async:
                self.spec.async_ = None
              elif self.spec.import_.create_async:
                self.spec.async_ = self.spec.import_.create_async
              # Reset command with updated configuration.
              self.ConfigureCommand()

          # Abort command early if no changes are detected.
          if abort_if_equivalent:
            if imported_resource == existing_resource:
              return log.status.Print(
                  'Request not sent for [{}]: No changes detected.'.format(
                      imported_resource.name))

        ref, response = self._CommonRun(
            args, existing_message=imported_resource)

        # Handle asynchronous behavior.
        if self.spec.async_:
          request_string = None
          if ref is not None:
            request_string = 'Request issued for: [{{{}}}]'.format(
                yaml_command_schema.NAME_FORMAT_KEY)
          response = self._HandleAsync(args, ref, response, request_string)

        return self._HandleResponse(response, args)

    return Command

  def _GenerateExportCommand(self):
    """Generates an export command.

    An export command has a single resource argument and an API method to call
    to get the resource. The result is exported to a local yaml file provided
    by the `--destination` flag, or to stdout if nothing is provided.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # Lazy import to prevent drag on startup time.
    from googlecloudsdk.command_lib.export import util as export_util  # pylint:disable=g-import-not-at-top

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.ExportCommand):
      """Export command enclosure."""

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        parser.add_argument(
            '--destination',
            help="""
            Path to a YAML file where the configuration will be exported.
            The exported data will not contain any output-only fields.
            Alternatively, you may omit this flag to write to standard output. A
            schema describing the export/import format can be found in
            $CLOUDSDKROOT/lib/googlecloudsdk/schemas/...
          """)

      def Run(self_, args):
        unused_ref, response = self._CommonRun(args)
        schema_path = export_util.GetSchemaPath(self.method.collection.api_name,
                                                self.spec.request.api_version,
                                                type(response).__name__)

        # Export parsed yaml to selected destination.
        if args.IsSpecified('destination'):
          with files.FileWriter(args.destination) as stream:
            export_util.Export(
                message=response, stream=stream, schema_path=schema_path)
          return log.status.Print('Exported [{}] to \'{}\'.'.format(
              response.name, args.destination))
        else:
          export_util.Export(
              message=response, stream=sys.stdout, schema_path=schema_path)

    return Command

  def _GenerateUpdateCommand(self):
    """Generates an update command.

    An update command has a resource argument, additional fields, and calls an
    API method. It supports async if the async configuration is given. Any
    fields is message_params will be generated as arguments and inserted into
    the request message.

    Currently, the Update command is the same as Generic command.

    Returns:
      calliope.base.Command, The command that implements the spec.
    """

    # pylint: disable=no-self-argument, The class closure throws off the linter
    # a bit. We want to use the generator class, not the class being generated.
    # pylint: disable=protected-access, The linter gets confused about 'self'
    # and thinks we are accessing something protected.
    class Command(base.Command):
      # pylint: disable=missing-docstring

      @staticmethod
      def Args(parser):
        self._CommonArgs(parser)
        if self.spec.async_:
          base.ASYNC_FLAG.AddToParser(parser)
        if self.spec.arguments.labels:
          labels_util.AddUpdateLabelsFlags(parser)

      def Run(self_, args):
        # Check if mask is required for an update request, if required, return
        # the dotted path, e.g updateRequest.fieldMask.
        mask_path = update.GetMaskFieldPath(self.method)
        if mask_path:
          # If user sets to disable the auto-generated field mask, set the value
          # to the empty string instead so that custom hooks can be used.
          if self.spec.update and self.spec.update.disable_auto_field_mask:
            mask_string = ''
          else:
            mask_string = update.GetMaskString(args, self.spec, mask_path)
          self.spec.request.static_fields[mask_path] = mask_string

        # Check if the update is full-update, which requires a get request.
        existing_message = None
        if self.spec.update:
          if self.spec.update.read_modify_update:
            existing_message = self._GetExistingResource(args)

        ref, response = self._CommonRun(args, existing_message)
        if self.spec.async_:
          request_string = None
          if ref:
            request_string = 'Request issued for: [{{{}}}]'.format(
                yaml_command_schema.NAME_FORMAT_KEY)
          response = self._HandleAsync(
              args, ref, response, request_string=request_string)

        log.UpdatedResource(
            self._GetDisplayName(ref, args), kind=self.display_resource_type)
        return self._HandleResponse(response, args)

    return Command

  def _CommonArgs(self, parser):
    """Performs argument actions common to all commands.

    Adds all generated arguments to the parser
    Sets the command output format if specified

    Args:
      parser: The argparse parser.
    """
    args = self.arg_generator.GenerateArgs()
    parser = self._Exclude(parser)
    for arg in args:
      arg.AddToParser(parser)
    if self.spec.arguments.additional_arguments_hook:
      for arg in self.spec.arguments.additional_arguments_hook():
        arg.AddToParser(parser)
    if self.spec.output.format:
      parser.display_info.AddFormat(self.spec.output.format)
    if self.spec.output.flatten:
      parser.display_info.AddFlatten(self.spec.output.flatten)

  def _Exclude(self, parser):
    """Excludes specified arguments from the parser.

    Args:
      parser: The argparse parser.

    Returns:
      The argparse parser.
    """
    for arg in self.spec.arguments.exclude:
      base.Argument('--{}'.format(arg), help='').RemoveFromParser(parser)
    return parser

  def _CommonRun(self, args, existing_message=None):
    """Performs run actions common to all commands.

    Parses the resource argument into a resource reference
    Prompts the user to continue (if applicable)
    Calls the API method with the request generated from the parsed arguments

    Args:
      args: The argparse parser.
      existing_message: the apitools message returned from previous request.

    Returns:
      (resources.Resource, response), A tuple of the parsed resource reference
      and the API response from the method call.
    """
    ref = self.arg_generator.GetRequestResourceRef(args)
    if self.spec.input.confirmation_prompt:
      console_io.PromptContinue(
          message=self._Format(self.spec.input.confirmation_prompt, ref,
                               self._GetDisplayName(ref, args)),
          default=self.spec.input.default_continue,
          throw_if_unattended=True, cancel_on_no=True)

    if self.spec.request.modify_method_hook:
      self.spec.request.method = self.spec.request.modify_method_hook(ref, args)
      self.method = registry.GetMethod(
          self.spec.request.collection, self.spec.request.method,
          self.spec.request.api_version)

    if self.spec.request.issue_request_hook:
      # Making the request is overridden, just call into the custom code.
      return ref, self.spec.request.issue_request_hook(ref, args)

    if self.spec.request.create_request_hook:
      # We are going to make the request, but there is custom code to create it.
      request = self.spec.request.create_request_hook(ref, args)
    else:
      parse_resource = self.spec.request.parse_resource_into_request
      request = self.arg_generator.CreateRequest(
          args,
          self.spec.request.static_fields,
          self.spec.request.resource_method_params,
          self.spec.arguments.labels,
          self.spec.command_type,
          use_relative_name=self.spec.request.use_relative_name,
          parse_resource_into_request=parse_resource,
          existing_message=existing_message,
          override_method=self.method)
      for hook in self.spec.request.modify_request_hooks:
        request = hook(ref, args, request)

    response = self.method.Call(request,
                                limit=self.arg_generator.Limit(args),
                                page_size=self.arg_generator.PageSize(args))
    return ref, response

  def _SetPolicyUpdateMask(self, update_mask):
    """Set Field Mask on SetIamPolicy request message.

    If the API supports update_masks then adds the update_mask to the
    SetIamPolicy request (via static fields).
    Args:
      update_mask: str, comma separated string listing the Policy fields to be
        updated.
    """
    # Standard names for SetIamPolicyRequest message and set IAM request
    # field name

    set_iam_policy_request = 'SetIamPolicyRequest'
    policy_request_path = 'setIamPolicyRequest'

    # Use SetIamPolicyRequest message and set IAM request field name overrides
    # for API's with non-standard naming (if provided)
    if self.spec.iam:
      overrides = self.spec.iam.message_type_overrides
      if 'set_iam_policy_request' in overrides:
        set_iam_policy_request = (overrides['set_iam_policy_request']
                                  or set_iam_policy_request)
      policy_request_path = (self.spec.iam.set_iam_policy_request_path
                             or policy_request_path)

    mask_field_path = '{}.updateMask'.format(policy_request_path)
    update_request = self.method.GetMessageByName(set_iam_policy_request)
    if hasattr(update_request, 'updateMask'):
      self.spec.request.static_fields[mask_field_path] = update_mask

  def _GetIamPolicy(self, args):
    """GetIamPolicy helper function for add/remove binding."""
    get_iam_method = registry.GetMethod(self.spec.request.collection,
                                        'getIamPolicy',
                                        self.spec.request.api_version)
    get_iam_request = self.arg_generator.CreateRequest(
        args,
        use_relative_name=self.spec.request.use_relative_name,
        override_method=get_iam_method)

    if self.spec.iam and self.spec.iam.policy_version:
      arg_utils.SetFieldInMessage(
          get_iam_request,
          self.spec.iam.get_iam_policy_version_path,
          self.spec.iam.policy_version)

    policy = get_iam_method.Call(get_iam_request)
    return policy

  def _GetModifiedIamPolicyAddIamBinding(self, args, add_condition=False):
    """Get the IAM policy and add the specified binding to it.

    Args:
      args: an argparse namespace.
      add_condition: True if support condition.

    Returns:
      IAM policy.
    """
    binding_message_type = self.method.GetMessageByName('Binding')
    if add_condition:
      condition = iam_util.ValidateAndExtractConditionMutexRole(args)
      policy = self._GetIamPolicy(args)
      condition_message_type = self.method.GetMessageByName('Expr')
      iam_util.AddBindingToIamPolicyWithCondition(
          binding_message_type, condition_message_type, policy, args.member,
          args.role, condition)
    else:
      policy = self._GetIamPolicy(args)
      iam_util.AddBindingToIamPolicy(binding_message_type, policy, args.member,
                                     args.role)
    return policy

  def _GetModifiedIamPolicyRemoveIamBinding(self, args, add_condition=False):
    """Get the IAM policy and remove the specified binding to it.

    Args:
      args: an argparse namespace.
      add_condition: True if support condition.

    Returns:
      IAM policy.
    """
    if add_condition:
      condition = iam_util.ValidateAndExtractCondition(args)
      policy = self._GetIamPolicy(args)
      iam_util.RemoveBindingFromIamPolicyWithCondition(
          policy, args.member, args.role, condition, all_conditions=args.all)
    else:
      policy = self._GetIamPolicy(args)
      iam_util.RemoveBindingFromIamPolicy(policy, args.member, args.role)
    return policy

  def _GetExistingResource(self, args):
    get_method = registry.GetMethod(self.spec.request.collection, 'get',
                                    self.spec.request.api_version)
    get_arg_generator = arg_marshalling.DeclarativeArgumentGenerator(
        get_method, [], self.spec.arguments.resource)

    # TODO(b/111069150): Add error handling when get fails.
    return get_method.Call(get_arg_generator.CreateRequest(args))

  def _HandleAsync(self, args, resource_ref, operation,
                   request_string, extract_resource_result=True):
    """Handles polling for operations if the async flag is provided.

    Args:
      args: argparse.Namespace, The parsed args.
      resource_ref: resources.Resource, The resource reference for the resource
        being operated on (not the operation itself)
      operation: The operation message response.
      request_string: The format string to print indicating a request has been
        issued for the resource. If None, nothing is printed.
      extract_resource_result: bool, True to return the original resource as
        the result or False to just return the operation response when it is
        done. You would set this to False for things like Delete where the
        resource no longer exists when the operation is done.

    Returns:
      The response (either the operation or the original resource).
    """
    operation_ref = resources.REGISTRY.Parse(
        getattr(operation, self.spec.async_.response_name_field),
        collection=self.spec.async_.collection)
    request_string = self.spec.async_.request_issued_message or request_string
    if request_string:
      log.status.Print(self._Format(request_string, resource_ref,
                                    self._GetDisplayName(resource_ref, args)))
    if args.async_:
      log.status.Print(self._Format(
          'Check operation [{{{}}}] for status.'
          .format(yaml_command_schema.REL_NAME_FORMAT_KEY), operation_ref))
      return operation

    return self._WaitForOperation(
        operation_ref, resource_ref, extract_resource_result, args=args)

  def _WaitForOperation(self, operation_ref, resource_ref,
                        extract_resource_result, args=None):
    poller = AsyncOperationPoller(
        self.spec, resource_ref if extract_resource_result else None, args)
    progress_string = self._Format(
        'Waiting for operation [{{{}}}] to complete'.format(
            yaml_command_schema.REL_NAME_FORMAT_KEY),
        operation_ref)
    return waiter.WaitFor(
        poller, operation_ref, self._Format(
            progress_string, resource_ref,
            self._GetDisplayName(resource_ref, args) if args else None))

  def _HandleResponse(self, response, args=None):
    """Process the API response.

    Args:
      response: The apitools message object containing the API response.
      args: argparse.Namespace, The parsed args.

    Raises:
      core.exceptions.Error: If an error was detected and extracted from the
        response.

    Returns:
      A possibly modified response.
    """
    if self.spec.response.error:
      error = self._FindPopulatedAttribute(
          response, self.spec.response.error.field.split('.'))
      if error:
        messages = []
        if self.spec.response.error.code:
          messages.append('Code: [{}]'.format(
              _GetAttribute(error, self.spec.response.error.code)))
        if self.spec.response.error.message:
          messages.append('Message: [{}]'.format(
              _GetAttribute(error, self.spec.response.error.message)))
        if messages:
          raise exceptions.Error(' '.join(messages))
        raise exceptions.Error(six.text_type(error))
    if self.spec.response.result_attribute:
      response = _GetAttribute(response, self.spec.response.result_attribute)
    for hook in self.spec.response.modify_response_hooks:
      response = hook(response, args)
    return response

  def _FindPopulatedAttribute(self, obj, attributes):
    """Searches the given object for an attribute that is non-None.

    This digs into the object search for the given attributes. If any attribute
    along the way is a list, it will search for sub-attributes in each item
    of that list. The first match is returned.

    Args:
      obj: The object to search
      attributes: [str], A sequence of attributes to use to dig into the
        resource.

    Returns:
      The first matching instance of the attribute that is non-None, or None
      if one could nto be found.
    """
    if not attributes:
      return obj
    attr = attributes[0]
    try:
      obj = getattr(obj, attr)
    except AttributeError:
      return None
    if isinstance(obj, list):
      for x in obj:
        obj = self._FindPopulatedAttribute(x, attributes[1:])
        if obj:
          return obj
    return self._FindPopulatedAttribute(obj, attributes[1:])

  def _Format(self, format_string, resource_ref, display_name=None):
    """Formats a string with all the attributes of the given resource ref.

    Args:
      format_string: str, The format string.
      resource_ref: resources.Resource, The resource reference to extract
        attributes from.
      display_name: the display name for the resource.

    Returns:
      str, The formatted string.
    """
    if resource_ref:
      d = resource_ref.AsDict()
      d[yaml_command_schema.NAME_FORMAT_KEY] = (
          display_name or resource_ref.Name())
      d[yaml_command_schema.REL_NAME_FORMAT_KEY] = resource_ref.RelativeName()
    else:
      d = {yaml_command_schema.NAME_FORMAT_KEY: display_name}
    d[yaml_command_schema.RESOURCE_TYPE_FORMAT_KEY] = self.display_resource_type
    return format_string.format(**d)

  def _RegisterURIFunc(self, args):
    """Generates and registers a function to create a URI from a resource.

    Args:
      args: The argparse namespace.

    Returns:
      f(resource) -> str, A function that converts the given resource payload
      into a URI.
    """
    def URIFunc(resource):
      id_value = getattr(
          resource, self.spec.response.id_field)
      ref = self.arg_generator.GetResponseResourceRef(id_value, args)
      return ref.SelfLink()
    args.GetDisplayInfo().AddUriFunc(URIFunc)

  def _ConfigureGlobalAttributes(self, command):
    """Configures top level attributes of the generated command.

    Args:
      command: The command being generated.
    """
    if self.spec.hidden:
      command = base.Hidden(command)
    if self.spec.release_tracks:
      command = base.ReleaseTracks(*self.spec.release_tracks)(command)
    if self.spec.deprecated_data:
      command = base.Deprecate(**self.spec.deprecated_data)(command)
    if not hasattr(command, 'detailed_help'):
      key_map = {
          'description': 'DESCRIPTION',
          'examples': 'EXAMPLES',
      }
      command.detailed_help = {
          key_map.get(k, k): v for k, v in self.spec.help_text.items()}
    command.detailed_help['API REFERENCE'] = (
        'This command uses the *{}/{}* API. The full documentation for this '
        'API can be found at: {}'.format(
            self.method.collection.api_name, self.method.collection.api_version,
            self.method.collection.docs_url))

  def _GetDisplayName(self, resource_ref, args):
    if (self.spec.arguments.resource
        and self.spec.arguments.resource.display_name_hook):
      return self.spec.arguments.resource.display_name_hook(resource_ref, args)
    return resource_ref.Name() if resource_ref else None


class AsyncOperationPoller(waiter.OperationPoller):
  """An implementation of a operation poller."""

  def __init__(self, spec, resource_ref, args):
    """Creates the poller.

    Args:
      spec: yaml_command_schema.CommandData, the spec for the command being
        generated.
      resource_ref: resources.Resource, The resource reference for the resource
        being operated on (not the operation itself). If None, the operation
        will just be returned when it is done instead of getting the resulting
        resource.
      args: Namespace, The args namespace.
    """
    self.spec = spec
    self.resource_ref = resource_ref
    if not self.spec.async_.extract_resource_result:
      self.resource_ref = None
    self.method = registry.GetMethod(
        spec.async_.collection, spec.async_.method,
        api_version=spec.async_.api_version or spec.request.api_version)
    self.args = args

  def IsDone(self, operation):
    """Overrides."""
    result = getattr(operation, self.spec.async_.state.field)
    if isinstance(result, apitools_messages.Enum):
      result = result.name
    if (result in self.spec.async_.state.success_values or
        result in self.spec.async_.state.error_values):
      # We found a value that means it is done.
      error = getattr(operation, self.spec.async_.error.field)
      if not error and result in self.spec.async_.state.error_values:
        error = 'The operation failed.'
      # If we succeeded but there is an error, or if an error was detected.
      if error:
        raise waiter.OperationError(SerializeError(error))
      return True

    return False

  def Poll(self, operation_ref):
    """Overrides.

    Args:
      operation_ref: googlecloudsdk.core.resources.Resource.

    Returns:
      fetched operation message.
    """
    request_type = self.method.GetRequestType()
    relative_name = operation_ref.RelativeName()
    fields = {
        f.name: getattr(  # pylint:disable=g-complex-comprehension
            operation_ref,
            self.spec.async_.operation_get_method_params.get(f.name, f.name),
            relative_name)
        for f in request_type.all_fields()}
    request = request_type(**fields)
    for hook in self.spec.async_.modify_request_hooks:
      request = hook(operation_ref, self.args, request)
    return self.method.Call(request)

  def GetResult(self, operation):
    """Overrides.

    Args:
      operation: api_name_messages.Operation.

    Returns:
      result of result_service.Get request.
    """
    result = operation
    if self.resource_ref:
      method = self._ResourceGetMethod()
      request = method.GetRequestType()()
      arg_utils.ParseResourceIntoMessage(self.resource_ref, method, request)
      result = method.Call(request)
    return _GetAttribute(result, self.spec.async_.result_attribute)

  def _ResourceGetMethod(self):
    return registry.GetMethod(
        self.spec.request.collection, self.spec.async_.resource_get_method,
        api_version=self.spec.request.api_version)


def SerializeError(error):
  """Serializes the error message for better format."""
  if isinstance(error, six.string_types):
    return error
  try:
    return json.dumps(
        encoding.MessageToDict(error),
        indent=2,
        sort_keys=True,
        separators=(',', ': '))
  except Exception:  # pylint: disable=broad-except
    # try the best, fall back to return error
    return error


def _GetAttribute(obj, attr_path):
  """Gets attributes and sub-attributes out of an object.

  Args:
    obj: The object to extract the attributes from.
    attr_path: str, The dotted path of attributes to extract.

  Raises:
    AttributeError: If the attribute doesn't exist on the object.

  Returns:
    The desired attribute or None if any of the parent attributes were None.
  """
  if attr_path:
    for attr in attr_path.split('.'):
      try:
        if obj is None:
          return None
        obj = getattr(obj, attr)
      except AttributeError:
        raise AttributeError(
            'Attribute path [{}] not found on type [{}]'.format(attr_path,
                                                                type(obj)))
  return obj
