# -*- 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.
"""Command for labels update to instances."""


from googlecloudsdk.api_lib.compute import base_classes
from googlecloudsdk.api_lib.compute import instance_utils
from googlecloudsdk.api_lib.compute import partner_metadata_utils
from googlecloudsdk.api_lib.compute.operations import poller
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.compute.instances import flags
from googlecloudsdk.command_lib.compute.sole_tenancy import flags as sole_tenancy_flags
from googlecloudsdk.command_lib.compute.sole_tenancy import util as sole_tenancy_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.args import labels_util


DETAILED_HELP = {
    'DESCRIPTION': """
        *{command}* updates labels and requested CPU Platform for a
        Compute
        Engine virtual machine.
    """,
    'EXAMPLES': """
    To modify the instance 'example-instance' in 'us-central1-a' by adding
    labels 'k0', with value 'value1' and label 'k1' with value 'value2' and
    removing labels with key 'k3', run:

      $ {command} example-instance --zone=us-central1-a --update-labels=k0=value1,k1=value2 --remove-labels=k3

    Labels can be used to identify the instance. To list instances with the 'k1:value2' label, run:

      $ {parent_command} list --filter='labels.k1:value2'

    To list only the labels when describing a resource, use --format to filter the result:

      $ {parent_command} describe example-instance --format="default(labels)"
  """
}


@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA, base.ReleaseTrack.PREVIEW)
class Update(base.UpdateCommand):
  """Update a Compute Engine virtual machine."""

  @staticmethod
  def Args(parser):

    flags.INSTANCE_ARG.AddArgument(parser, operation_type='update')
    labels_util.AddUpdateLabelsFlags(parser)
    flags.AddMinCpuPlatformArgs(parser, Update.ReleaseTrack())
    flags.AddDeletionProtectionFlag(parser, use_default_value=False)
    flags.AddShieldedInstanceConfigArgs(
        parser, use_default_value=False, for_update=True)
    flags.AddShieldedInstanceIntegrityPolicyArgs(parser)
    flags.AddDisplayDeviceArg(parser, is_update=True)
    sole_tenancy_flags.AddNodeAffinityFlagToParser(parser, is_update=True)

  def Run(self, args):
    return self._Run(args)

  def _Run(self, args):
    holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
    client = holder.client.apitools_client
    messages = holder.client.messages

    instance_ref = flags.INSTANCE_ARG.ResolveAsResource(
        args, holder.resources,
        scope_lister=flags.GetInstanceZoneScopeLister(holder.client))

    most_disruptive_allowed_action_enum = (
        messages.ComputeInstancesUpdateRequest.MostDisruptiveAllowedActionValueValuesEnum
    )
    most_disruptive_allowed_action_choice = getattr(
        args, 'most_disruptive_allowed_action', 'REFRESH'
    )
    most_disruptive_allowed_action = arg_utils.ChoiceToEnum(
        most_disruptive_allowed_action_choice,
        most_disruptive_allowed_action_enum,
    )

    minimal_action = (
        messages.ComputeInstancesUpdateRequest.MinimalActionValueValuesEnum.NO_EFFECT
    )
    if getattr(args, 'minimal_action', None) is not None:
      minimal_action = arg_utils.ChoiceToEnum(
          args.minimal_action,
          messages.ComputeInstancesUpdateRequest.MinimalActionValueValuesEnum,
      )

    restart_only_args = ['identity', 'identity_certificate']
    specified_restart_only_args = []
    for arg in restart_only_args:
      if hasattr(args, arg) and args.IsSpecified(arg):
        specified_restart_only_args.append(arg)

    if specified_restart_only_args:
      if (
          most_disruptive_allowed_action
          != most_disruptive_allowed_action_enum.RESTART
      ):
        raise calliope_exceptions.InvalidArgumentException(
            '--most-disruptive-allowed-action',
            '[--most-disruptive-allowed-action] must be set to RESTART if '
            '[{}] is specified.'.format(
                ' or '.join([
                    '--' + arg.replace('_', '-')
                    for arg in specified_restart_only_args
                ])
            ),
        )

    result = None

    labels_operation_ref = None
    min_cpu_platform_operation_ref = None
    deletion_protection_operation_ref = None
    shielded_instance_config_ref = None
    display_device_ref = None
    partner_metadata_operation_ref = None
    graceful_shutdown_operation_ref = None

    labels_diff = labels_util.Diff.FromUpdateArgs(args)
    if labels_diff.MayHaveUpdates():
      instance = client.instances.Get(
          messages.ComputeInstancesGetRequest(**instance_ref.AsDict()))
      result = instance
      labels_operation_ref = self._GetLabelsOperationRef(
          labels_diff, instance, instance_ref, holder)
    if hasattr(args, 'min_cpu_platform') and args.min_cpu_platform is not None:
      min_cpu_platform_operation_ref = self._GetMinCpuPlatformOperationRef(
          args.min_cpu_platform, instance_ref, holder)
    if args.deletion_protection is not None:
      deletion_protection_operation_ref = (
          self._GetDeletionProtectionOperationRef(
              args.deletion_protection, instance_ref, holder))
    if hasattr(args, 'partner_metadata') and (
        args.partner_metadata or args.partner_metadata_from_file
    ):
      partner_metadata_operation_ref = self._GetPartnerMetadataOperationRef(
          args, instance_ref, holder, most_disruptive_allowed_action,
          minimal_action
      )
    if (
        hasattr(args, 'graceful_shutdown')
        and args.IsSpecified('graceful_shutdown')
    ) or (
        hasattr(args, 'graceful_shutdown_max_duration')
        and args.IsSpecified('graceful_shutdown_max_duration')
    ):
      graceful_shutdown_operation_ref = self._GetGracefulShutdownOperationRef(
          args, instance_ref, holder, most_disruptive_allowed_action,
          minimal_action
      )

    # Update instance fields requiring restart.
    instance_update_operation_ref = None
    if specified_restart_only_args:
      instance_update_operation_ref = (
          self._GetUpdateInstanceRefRequiringRestart(
              instance_ref, args, holder, most_disruptive_allowed_action,
              minimal_action
          )
      )

    operation_poller = poller.Poller(client.instances)
    result = (
        self._WaitForResult(
            operation_poller,
            labels_operation_ref,
            'Updating labels of instance [{0}]',
            instance_ref.Name(),
        )
        or result
    )
    result = (
        self._WaitForResult(
            operation_poller,
            min_cpu_platform_operation_ref,
            'Changing minimum CPU platform of instance [{0}]',
            instance_ref.Name(),
        )
        or result
    )
    result = (
        self._WaitForResult(
            operation_poller,
            deletion_protection_operation_ref,
            'Setting deletion protection of instance [{0}] to [{1}]',
            instance_ref.Name(),
            args.deletion_protection,
        )
        or result
    )
    result = (
        self._WaitForResult(
            operation_poller,
            partner_metadata_operation_ref,
            'Updating partner metadata of instance [{0}]',
            instance_ref.Name(),
        )
        or result
    )
    result = (
        self._WaitForResult(
            operation_poller,
            graceful_shutdown_operation_ref,
            'Updating graceful shutdown configuration of instance [{0}]',
            instance_ref.Name(),
        )
        or result
    )
    result = (
        self._WaitForResult(
            operation_poller,
            instance_update_operation_ref,
            'Updating instance [{0}] fields that require restart',
            instance_ref.Name(),
        )
        or result
    )

    if (args.IsSpecified('shielded_vm_secure_boot') or
        args.IsSpecified('shielded_vm_vtpm') or
        args.IsSpecified('shielded_vm_integrity_monitoring')):
      shielded_instance_config_ref = self._GetShieldedInstanceConfigRef(
          instance_ref, args, holder)
      result = self._WaitForResult(
          operation_poller, shielded_instance_config_ref,
          'Setting shieldedInstanceConfig  of instance [{0}]',
          instance_ref.Name()) or result

    if args.IsSpecified('shielded_vm_learn_integrity_policy'):
      shielded_instance_integrity_policy_ref = (
          self._GetShieldedInstanceIntegrityPolicyRef(instance_ref, holder))
      result = self._WaitForResult(
          operation_poller, shielded_instance_integrity_policy_ref,
          'Setting shieldedInstanceIntegrityPolicy of instance [{0}]',
          instance_ref.Name()) or result

    if args.IsSpecified('enable_display_device'):
      display_device_ref = self._GetDisplayDeviceOperationRef(
          args.enable_display_device, instance_ref, holder)
      result = self._WaitForResult(operation_poller, display_device_ref,
                                   'Updating display device of instance [{0}]',
                                   instance_ref.Name()) or result

    if instance_utils.IsAnySpecified(args, 'node', 'node_affinity_file',
                                     'node_group', 'clear_node_affinities'):
      update_scheduling_ref = self._GetUpdateInstanceSchedulingRef(
          instance_ref, args, holder, most_disruptive_allowed_action,
          minimal_action)
      result = self._WaitForResult(
          operation_poller,
          update_scheduling_ref, 'Updating the scheduling of instance [{0}]',
          instance_ref.Name()) or result

    return result

  def _GetUpdateInstanceSchedulingRef(self, instance_ref, args, holder,
                                      most_disruptive_allowed_action,
                                      minimal_action):
    client = holder.client.apitools_client
    messages = holder.client.messages
    if instance_utils.IsAnySpecified(args, 'node', 'node_affinity_file',
                                     'node_group'):
      affinities = sole_tenancy_util.GetSchedulingNodeAffinityListFromArgs(
          args, messages)
    elif args.IsSpecified('clear_node_affinities'):
      affinities = []
    else:
      # No relevant args were specified. We shouldn't have called this function.
      return None
    instance = client.instances.Get(
        messages.ComputeInstancesGetRequest(**instance_ref.AsDict()))
    instance.scheduling.nodeAffinities = affinities

    request = messages.ComputeInstancesUpdateRequest(
        instance=instance_ref.Name(),
        project=instance_ref.project,
        zone=instance_ref.zone,
        instanceResource=instance,
        minimalAction=minimal_action,
        mostDisruptiveAllowedAction=most_disruptive_allowed_action)

    operation = client.instances.Update(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _GetShieldedInstanceConfigRef(self, instance_ref, args, holder):
    client = holder.client.apitools_client
    messages = holder.client.messages

    if (args.shielded_vm_secure_boot is None and
        args.shielded_vm_vtpm is None and
        args.shielded_vm_integrity_monitoring is None):
      return None
    shieldedinstance_config_message = (
        instance_utils.CreateShieldedInstanceConfigMessage(
            messages,
            args.shielded_vm_secure_boot,
            args.shielded_vm_vtpm,
            args.shielded_vm_integrity_monitoring)
        )

    request = messages.ComputeInstancesUpdateShieldedInstanceConfigRequest(
        instance=instance_ref.Name(),
        project=instance_ref.project,
        shieldedInstanceConfig=shieldedinstance_config_message,
        zone=instance_ref.zone)

    operation = client.instances.UpdateShieldedInstanceConfig(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _GetShieldedInstanceIntegrityPolicyRef(self, instance_ref, holder):
    client = holder.client.apitools_client
    messages = holder.client.messages

    shieldedinstance_integrity_policy_message = (
        instance_utils.CreateShieldedInstanceIntegrityPolicyMessage(messages))

    request = messages.ComputeInstancesSetShieldedInstanceIntegrityPolicyRequest(
        instance=instance_ref.Name(),
        project=instance_ref.project,
        shieldedInstanceIntegrityPolicy=shieldedinstance_integrity_policy_message,
        zone=instance_ref.zone)

    operation = client.instances.SetShieldedInstanceIntegrityPolicy(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _GetLabelsOperationRef(self, labels_diff, instance, instance_ref, holder):
    client = holder.client.apitools_client
    messages = holder.client.messages

    labels_update = labels_diff.Apply(
        messages.InstancesSetLabelsRequest.LabelsValue,
        instance.labels)

    if labels_update.needs_update:
      request = messages.ComputeInstancesSetLabelsRequest(
          project=instance_ref.project,
          instance=instance_ref.instance,
          zone=instance_ref.zone,
          instancesSetLabelsRequest=
          messages.InstancesSetLabelsRequest(
              labelFingerprint=instance.labelFingerprint,
              labels=labels_update.labels))

      operation = client.instances.SetLabels(request)
      return holder.resources.Parse(
          operation.selfLink, collection='compute.zoneOperations')

  def _GetMinCpuPlatformOperationRef(self, min_cpu_platform, instance_ref,
                                     holder):
    client = holder.client.apitools_client
    messages = holder.client.messages
    embedded_request = messages.InstancesSetMinCpuPlatformRequest(
        minCpuPlatform=min_cpu_platform or None)
    request = messages.ComputeInstancesSetMinCpuPlatformRequest(
        instance=instance_ref.instance,
        project=instance_ref.project,
        instancesSetMinCpuPlatformRequest=embedded_request,
        zone=instance_ref.zone)

    operation = client.instances.SetMinCpuPlatform(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _GetDeletionProtectionOperationRef(self, deletion_protection,
                                         instance_ref, holder):
    client = holder.client.apitools_client
    messages = holder.client.messages
    request = messages.ComputeInstancesSetDeletionProtectionRequest(
        deletionProtection=deletion_protection,
        project=instance_ref.project,
        resource=instance_ref.instance,
        zone=instance_ref.zone)

    operation = client.instances.SetDeletionProtection(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _GetDisplayDeviceOperationRef(self, display_device, instance_ref, holder):
    client = holder.client.apitools_client
    messages = holder.client.messages
    request = messages.ComputeInstancesUpdateDisplayDeviceRequest(
        displayDevice=messages.DisplayDevice(
            enableDisplay=display_device),
        project=instance_ref.project,
        instance=instance_ref.instance,
        zone=instance_ref.zone)

    operation = client.instances.UpdateDisplayDevice(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _GetGracefulShutdownOperationRef(self, args, instance_ref, holder,
                                       most_disruptive_allowed_action,
                                       minimal_action):
    messages = holder.client.messages
    client = holder.client.apitools_client
    instance = client.instances.Get(
        messages.ComputeInstancesGetRequest(**instance_ref.AsDict())
    )

    updated_graceful_shutdown_message = instance.scheduling.gracefulShutdown
    if hasattr(args, 'graceful_shutdown') and args.IsSpecified(
        'graceful_shutdown'
    ):
      if updated_graceful_shutdown_message is None:
        updated_graceful_shutdown_message = (
            messages.SchedulingGracefulShutdown()
        )
      updated_graceful_shutdown_message.enabled = args.graceful_shutdown
    if hasattr(args, 'graceful_shutdown_max_duration') and args.IsSpecified(
        'graceful_shutdown_max_duration'
    ):
      if updated_graceful_shutdown_message is None:
        updated_graceful_shutdown_message = (
            messages.SchedulingGracefulShutdown()
        )
      updated_graceful_shutdown_message.maxDuration = messages.Duration(
          seconds=args.graceful_shutdown_max_duration
      )

    instance.scheduling.gracefulShutdown = updated_graceful_shutdown_message

    request = messages.ComputeInstancesUpdateRequest(
        instance=instance_ref.Name(),
        project=instance_ref.project,
        zone=instance_ref.zone,
        instanceResource=instance,
        minimalAction=minimal_action,
        mostDisruptiveAllowedAction=most_disruptive_allowed_action,
    )

    operation = client.instances.Update(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations'
    )

  def _GetPartnerMetadataOperationRef(self, args, instance_ref, holder,
                                      most_disruptive_allowed_action,
                                      minimal_action):
    messages = holder.client.messages
    client = holder.client.apitools_client
    partner_metadata_dict = (
        partner_metadata_utils.CreatePartnerMetadataDict(args)
    )
    partner_metadata_utils.ValidatePartnerMetadata(partner_metadata_dict)
    partner_metadata_message = messages.Instance.PartnerMetadataValue()
    for namespace, structured_entries in partner_metadata_dict.items():
      partner_metadata_message.additionalProperties.append(
          messages.Instance.PartnerMetadataValue.AdditionalProperty(
              key=namespace,
              value=partner_metadata_utils.ConvertStructuredEntries(
                  structured_entries, messages
              ),
          )
      )
    instance = client.instances.Get(
        messages.ComputeInstancesGetRequest(**instance_ref.AsDict()))
    instance.partnerMetadata = partner_metadata_message
    request = messages.ComputeInstancesUpdateRequest(
        instance=instance_ref.Name(),
        project=instance_ref.project,
        zone=instance_ref.zone,
        instanceResource=instance,
        minimalAction=minimal_action,
        mostDisruptiveAllowedAction=most_disruptive_allowed_action)

    operation = client.instances.Update(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _UpdateWorkloadIdentityConfig(self, args, instance, messages):
    create_config = False
    if instance.workloadIdentityConfig is None:
      for arg in ['identity', 'identity_certificate']:
        if hasattr(args, arg) and args.IsSpecified(arg):
          create_config = True
          break
    if create_config:
      instance.workloadIdentityConfig = messages.WorkloadIdentityConfig()

    if hasattr(args, 'identity') and args.IsSpecified('identity'):
      instance.workloadIdentityConfig.identity = args.identity
    if hasattr(args, 'identity_certificate') and args.IsSpecified(
        'identity_certificate'
    ):
      instance.workloadIdentityConfig.identityCertificateEnabled = (
          args.identity_certificate
      )

  def _GetUpdateInstanceRefRequiringRestart(self, instance_ref, args, holder,
                                            most_disruptive_allowed_action,
                                            minimal_action):
    client = holder.client.apitools_client
    messages = holder.client.messages
    instance = client.instances.Get(
        messages.ComputeInstancesGetRequest(**instance_ref.AsDict()))

    self._UpdateWorkloadIdentityConfig(args, instance, messages)
    # In future, call here other methods updating fields requiring restart.

    request = messages.ComputeInstancesUpdateRequest(
        instance=instance_ref.Name(),
        project=instance_ref.project,
        zone=instance_ref.zone,
        instanceResource=instance,
        minimalAction=minimal_action,
        mostDisruptiveAllowedAction=most_disruptive_allowed_action,
    )

    operation = client.instances.Update(request)
    return holder.resources.Parse(
        operation.selfLink, collection='compute.zoneOperations')

  def _WaitForResult(self, operation_poller, operation_ref, message, *args):
    if operation_ref:
      return waiter.WaitFor(
          operation_poller, operation_ref, message.format(*args))
    return None


@base.ReleaseTracks(base.ReleaseTrack.BETA)
class UpdateBeta(Update):
  """Update a Compute Engine virtual machine."""

  @staticmethod
  def Args(parser):
    flags.INSTANCE_ARG.AddArgument(parser, operation_type='update')
    labels_util.AddUpdateLabelsFlags(parser)
    flags.AddMinCpuPlatformArgs(parser, UpdateBeta.ReleaseTrack())
    flags.AddDeletionProtectionFlag(parser, use_default_value=False)
    flags.AddShieldedInstanceConfigArgs(
        parser, use_default_value=False, for_update=True)
    flags.AddShieldedInstanceIntegrityPolicyArgs(parser)
    flags.AddDisplayDeviceArg(parser, is_update=True)
    sole_tenancy_flags.AddNodeAffinityFlagToParser(parser, is_update=True)
    partner_metadata_utils.AddPartnerMetadataArgs(parser)
    flags.AddGracefulShutdownArgs(parser)

  def Run(self, args):
    return self._Run(args)


@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class UpdateAlpha(UpdateBeta):
  """Update a Compute Engine virtual machine."""

  @staticmethod
  def Args(parser):
    flags.INSTANCE_ARG.AddArgument(parser, operation_type='update')
    labels_util.AddUpdateLabelsFlags(parser)
    flags.AddMinCpuPlatformArgs(parser, UpdateAlpha.ReleaseTrack())
    flags.AddDeletionProtectionFlag(parser, use_default_value=False)
    flags.AddShieldedInstanceConfigArgs(
        parser, use_default_value=False, for_update=True)
    flags.AddShieldedInstanceIntegrityPolicyArgs(parser)
    flags.AddDisplayDeviceArg(parser, is_update=True)
    sole_tenancy_flags.AddNodeAffinityFlagToParser(parser, is_update=True)
    partner_metadata_utils.AddPartnerMetadataArgs(parser)
    flags.AddGracefulShutdownArgs(parser)
    flags.AddWorkloadIdentityConfigArgs(parser)
    flags.AddMostDisruptiveAllowedActionArgs(parser)
    flags.AddMinimalActionArgs(parser)

Update.detailed_help = DETAILED_HELP
