# -*- 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 updating managed instance group."""


import functools

from googlecloudsdk.api_lib.compute import base_classes
from googlecloudsdk.api_lib.compute import managed_instance_groups_utils
from googlecloudsdk.api_lib.compute import utils
from googlecloudsdk.api_lib.compute.instance_groups.managed import stateful_policy_utils as policy_utils
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.compute import flags
from googlecloudsdk.command_lib.compute import scope as compute_scope
from googlecloudsdk.command_lib.compute.instance_groups import flags as instance_groups_flags
from googlecloudsdk.command_lib.compute.instance_groups.managed import flags as managed_flags
from googlecloudsdk.command_lib.compute.managed_instance_groups import auto_healing_utils
from googlecloudsdk.command_lib.util.apis import arg_utils
import six


# Flags valid only for regional MIGs.
REGIONAL_FLAGS = [
    'instance_redistribution_type',
    'target_distribution_shape',
    'on_repair_allow_changing_zone',
]

INSTANCE_TEMPLATE_ARG = managed_flags.InstanceTemplateArg(
    required=False,
    help_override="""
        Specifies the instance template that will replace the existing template used by the MIG.
        The MIG then uses this new template to create instances. Specifying the new template overrides
        any template versions that you've set for the MIG. Specify either a global or a regional instance template.
        """,
)


# TODO(b/345166947) Remove universe annotation once b/341682289 is resolved.
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA, base.ReleaseTrack.PREVIEW)
class UpdateGA(base.UpdateCommand):
  r"""Update a Compute Engine managed instance group."""

  support_update_policy_min_ready_flag = False
  support_multi_mig_flag = False
  support_instance_selection_min_cpu_platform = False
  support_flex_policy_flag_with_mincpu_and_disks = False

  @classmethod
  def Args(cls, parser):
    instance_groups_flags.MULTISCOPE_INSTANCE_GROUP_MANAGER_ARG.AddArgument(
        parser, operation_type='update'
    )

    autohealing_group = parser.add_mutually_exclusive_group()
    autohealing_group.add_argument(
        '--clear-autohealing',
        action='store_true',
        default=None,
        help="""\
        Clears all autohealing policy fields for the managed instance group.
        """,
    )
    autohealing_params_group = autohealing_group.add_group()
    auto_healing_utils.AddAutohealingArgs(autohealing_params_group)
    instance_groups_flags.AddMigUpdateStatefulFlags(parser)
    instance_groups_flags.AddMigUpdateStatefulFlagsIPs(parser)
    instance_groups_flags.AddDescriptionFlag(parser, for_update=True)
    managed_flags.AddMigInstanceRedistributionTypeFlag(parser)
    managed_flags.AddMigDistributionPolicyTargetShapeFlag(parser)
    managed_flags.AddMigListManagedInstancesResultsFlag(parser)
    INSTANCE_TEMPLATE_ARG.AddArgument(parser)
    managed_flags.AddMigUpdatePolicyFlags(
        parser, support_min_ready_flag=cls.support_update_policy_min_ready_flag
    )
    managed_flags.AddMigForceUpdateOnRepairFlags(parser)
    managed_flags.AddMigDefaultActionOnVmFailure(parser, cls.ReleaseTrack())
    managed_flags.AddMigActionOnVmFailedHealthCheck(parser)
    managed_flags.AddMigSizeFlag(parser)
    flags.AddInstanceFlexibilityPolicyArgs(
        parser,
        is_update=True,
        support_instance_selection_min_cpu_platform=cls.support_instance_selection_min_cpu_platform,
        support_flex_policy_flag_with_mincpu_and_disks=cls.support_flex_policy_flag_with_mincpu_and_disks,
    )
    managed_flags.AddStandbyPolicyFlags(parser)
    managed_flags.AddWorkloadPolicyFlags(parser)
    if cls.support_multi_mig_flag:
      managed_flags.AddMultiMigFlags(parser)
    # When adding RMIG-specific flag, update REGIONAL_FLAGS constant.

  def _GetUpdatedStatefulPolicyForDisks(
      self,
      client,
      current_stateful_policy,
      update_disks=None,
      remove_device_names=None,
  ):
    patched_disks_map = {}
    if remove_device_names:
      managed_instance_groups_utils.RegisterCustomStatefulDisksPatchEncoders(
          client
      )
    else:
      # Extract disk protos from current stateful policy proto
      if (
          current_stateful_policy
          and current_stateful_policy.preservedState
          and current_stateful_policy.preservedState.disks
      ):
        current_disks = (
            current_stateful_policy.preservedState.disks.additionalProperties
        )
      else:
        current_disks = []
      # Map of disks to have in the stateful policy, after updating and removing
      # the disks specified by the update and remove flags.
      patched_disks_map = {
          disk_entry.key: disk_entry for disk_entry in current_disks
      }

    # Update the disks specified in --stateful-disk
    for update_disk in update_disks or []:
      device_name = update_disk.get('device-name')
      updated_preserved_state_disk = (
          policy_utils.MakeStatefulPolicyPreservedStateDiskEntry(
              client.messages, update_disk
          )
      )
      # Patch semantics on the `--stateful-disk` flag
      if device_name in patched_disks_map:
        policy_utils.PatchStatefulPolicyDisk(
            patched_disks_map[device_name], updated_preserved_state_disk
        )
      else:
        patched_disks_map[device_name] = updated_preserved_state_disk

    # Remove the disks specified in --remove-stateful-disks
    for device_name in remove_device_names or []:
      patched_disks_map[device_name] = (
          policy_utils.MakeDiskDeviceNullEntryForDisablingInPatch(
              client, device_name
          )
      )

    stateful_disks = sorted(
        [
            stateful_disk
            for _, stateful_disk in six.iteritems(patched_disks_map)
        ],
        key=lambda x: x.key,
    )
    return stateful_disks

  def _GetUpdatedStatefulPolicy(self, client, current_stateful_policy, args):
    """Create an updated stateful policy based on specified args."""
    update_disks = args.stateful_disk
    remove_device_names = args.remove_stateful_disks
    stateful_disks = self._GetUpdatedStatefulPolicyForDisks(
        client, current_stateful_policy, update_disks, remove_device_names
    )

    stateful_policy = policy_utils.MakeStatefulPolicy(
        client.messages, stateful_disks
    )

    stateful_internal_ips = self._GetPatchForStatefulPolicyForInternalIPs(
        client, args.stateful_internal_ip, args.remove_stateful_internal_ips
    )

    stateful_external_ips = self._GetPatchForStatefulPolicyForExternalIPs(
        client, args.stateful_external_ip, args.remove_stateful_external_ips
    )

    return policy_utils.UpdateStatefulPolicy(
        client.messages,
        stateful_policy,
        None,
        stateful_internal_ips,
        stateful_external_ips,
    )

  def _StatefulArgsSet(self, args):
    return (
        args.IsSpecified('stateful_disk')
        or args.IsSpecified('remove_stateful_disks')
        or args.IsSpecified('stateful_internal_ip')
        or args.IsSpecified('remove_stateful_internal_ips')
        or args.IsSpecified('stateful_external_ip')
        or args.IsSpecified('remove_stateful_external_ips')
    )

  def _StatefulnessIntroduced(self, args):
    return (
        args.IsSpecified('stateful_disk')
        or args.IsSpecified('stateful_internal_ip')
        or args.IsSpecified('stateful_external_ip')
    )

  def _ValidateStatefulPolicyParams(self, args, stateful_policy):
    instance_groups_flags.ValidateUpdateStatefulPolicyParams(
        args, stateful_policy
    )
    instance_groups_flags.ValidateUpdateStatefulPolicyParamsWithIPs(
        args, stateful_policy
    )

  def _GetStatefulPolicyPatchForStatefulIPsCommon(
      self,
      client,
      update_ip_to_ip_entry_lambda,
      update_ip_to_none_lambda,
      update_ips=None,
      remove_interface_names=None,
  ):
    if remove_interface_names:
      managed_instance_groups_utils.RegisterCustomStatefulIpsPatchEncoders(
          client
      )
    patched_ips_map = {}

    # Update the interfaces specified in IPs to be updated.
    for update_ip in update_ips or []:
      # Interface name is optional, use the default if not specified.
      interface_name = update_ip.get(
          'interface-name',
          instance_groups_flags.STATEFUL_IP_DEFAULT_INTERFACE_NAME,
      )
      updated_preserved_state_ip = update_ip_to_ip_entry_lambda(update_ip)
      patched_ips_map[interface_name] = updated_preserved_state_ip

    # Remove the interfaces specified for removal.
    for interface_name in remove_interface_names or []:
      updated_preserved_state_ip = update_ip_to_none_lambda(interface_name)
      patched_ips_map[interface_name] = updated_preserved_state_ip

    stateful_ips = sorted(
        [stateful_ip for key, stateful_ip in six.iteritems(patched_ips_map)],
        key=lambda x: x.key,
    )
    return stateful_ips

  def _GetPatchForStatefulPolicyForInternalIPs(
      self, client, update_internal_ips=None, remove_interface_names=None
  ):
    # Extract internal IPs protos from current stateful policy proto.
    return self._GetStatefulPolicyPatchForStatefulIPsCommon(
        client,
        functools.partial(policy_utils.MakeInternalIPEntry, client.messages),
        functools.partial(
            policy_utils.MakeInternalIPNullEntryForDisablingInPatch, client
        ),
        update_internal_ips,
        remove_interface_names,
    )

  def _GetPatchForStatefulPolicyForExternalIPs(
      self, client, update_external_ips=None, remove_interface_names=None
  ):
    return self._GetStatefulPolicyPatchForStatefulIPsCommon(
        client,
        functools.partial(policy_utils.MakeExternalIPEntry, client.messages),
        functools.partial(
            policy_utils.MakeExternalIPNullEntryForDisablingInPatch, client
        ),
        update_external_ips,
        remove_interface_names,
    )

  def _PatchStatefulPolicy(self, igm_patch, args, igm_resource, client, holder):
    """Patch the stateful policy specified in args, to igm_patch."""
    # If we're potentially introducing statefulness to the MIG, we should
    # validate if this MIG is allowed to be stateful
    if self._StatefulnessIntroduced(args):
      managed_instance_groups_utils.ValidateIgmReadyForStatefulness(
          igm_resource, client
      )
    self._ValidateStatefulPolicyParams(args, igm_resource.statefulPolicy)

    igm_patch.statefulPolicy = self._GetUpdatedStatefulPolicy(
        client, igm_resource.statefulPolicy, args
    )
    return igm_patch

  def _GetValidatedAutohealingPolicies(
      self, holder, client, args, igm_resource
  ):
    health_check = managed_instance_groups_utils.GetHealthCheckUri(
        holder.resources, args
    )
    auto_healing_policies = (
        managed_instance_groups_utils.ModifyAutohealingPolicies(
            igm_resource.autoHealingPolicies,
            client.messages,
            args,
            health_check,
        )
    )
    managed_instance_groups_utils.ValidateAutohealingPolicies(
        auto_healing_policies
    )
    return auto_healing_policies

  def _GetInstanceTemplateRef(self, args, holder, default_scope):
    return INSTANCE_TEMPLATE_ARG.ResolveAsResource(
        args,
        holder.resources,
        default_scope=default_scope,
    )

  def _PatchTargetDistributionShape(
      self,
      patch_instance_group_manager,
      target_distribution_shape,
      igm_resource,
      client,
  ):
    distribution_policy = igm_resource.distributionPolicy
    if distribution_policy is None:
      distribution_policy = client.messages.DistributionPolicy()
    distribution_policy.targetShape = arg_utils.ChoiceToEnum(
        target_distribution_shape,
        client.messages.DistributionPolicy.TargetShapeValueValuesEnum,
    )
    patch_instance_group_manager.distributionPolicy = distribution_policy

  def _MakePatchRequest(self, client, igm_ref, igm_updated_resource):
    if igm_ref.Collection() == 'compute.instanceGroupManagers':
      service = client.apitools_client.instanceGroupManagers
      request = client.messages.ComputeInstanceGroupManagersPatchRequest(
          instanceGroupManager=igm_ref.Name(),
          instanceGroupManagerResource=igm_updated_resource,
          project=igm_ref.project,
          zone=igm_ref.zone,
      )
    else:
      service = client.apitools_client.regionInstanceGroupManagers
      request = client.messages.ComputeRegionInstanceGroupManagersPatchRequest(
          instanceGroupManager=igm_ref.Name(),
          instanceGroupManagerResource=igm_updated_resource,
          project=igm_ref.project,
          region=igm_ref.region,
      )
    return client.MakeRequests([(service, 'Patch', request)])

  def _GetRegionForGroup(self, igm_ref):
    if igm_ref.Collection() == 'compute.instanceGroupManagers':
      return utils.ZoneNameToRegionName(igm_ref.zone)
    else:
      return igm_ref.region

  def _CreateInstanceGroupManagerPatch(
      self, args, igm_ref, igm_resource, client, holder
  ):
    """Create IGM resource patch."""
    managed_flags.ValidateRegionalMigFlagsUsage(args, REGIONAL_FLAGS, igm_ref)
    patch_instance_group_manager = client.messages.InstanceGroupManager()
    include_fields = []
    auto_healing_policies = self._GetValidatedAutohealingPolicies(
        holder, client, args, igm_resource
    )
    if auto_healing_policies is not None:
      patch_instance_group_manager.autoHealingPolicies = auto_healing_policies

    if args.IsSpecified('template'):
      template_ref = self._GetInstanceTemplateRef(
          args, holder, compute_scope.ScopeEnum.GLOBAL
      )
      patch_instance_group_manager.instanceTemplate = template_ref.SelfLink()
      patch_instance_group_manager.versions = [
          client.messages.InstanceGroupManagerVersion(
              instanceTemplate=template_ref.SelfLink()
          )
      ]

    update_policy = managed_instance_groups_utils.PatchUpdatePolicy(
        client, args, igm_resource.updatePolicy
    )
    if update_policy is not None:
      patch_instance_group_manager.updatePolicy = update_policy

    if self._StatefulArgsSet(args):
      patch_instance_group_manager = self._PatchStatefulPolicy(
          patch_instance_group_manager, args, igm_resource, client, holder
      )
    if args.target_distribution_shape:
      self._PatchTargetDistributionShape(
          patch_instance_group_manager,
          args.target_distribution_shape,
          igm_resource,
          client,
      )
    if args.IsSpecified('description'):
      patch_instance_group_manager.description = args.description
    if args.IsSpecified('list_managed_instances_results'):
      patch_instance_group_manager.listManagedInstancesResults = (
          client.messages.InstanceGroupManager.ListManagedInstancesResultsValueValuesEnum
      )(args.list_managed_instances_results.upper())

    patch_instance_group_manager.instanceLifecyclePolicy = (
        managed_instance_groups_utils.CreateInstanceLifecyclePolicy(
            client.messages, args
        )
    )
    patch_instance_group_manager.instanceFlexibilityPolicy = managed_instance_groups_utils.CreateInstanceFlexibilityPolicy(
        args,
        client.messages,
        igm_resource,
        support_instance_selection_min_cpu_platform=self.support_instance_selection_min_cpu_platform,
    )
    if args.IsSpecified('size'):
      patch_instance_group_manager.targetSize = args.size

    standby_policy = managed_instance_groups_utils.CreateStandbyPolicy(
        client.messages,
        args.standby_policy_initial_delay,
        args.standby_policy_mode,
    )
    if standby_policy:
      patch_instance_group_manager.standbyPolicy = standby_policy
    if args.IsKnownAndSpecified('suspended_size'):
      patch_instance_group_manager.targetSuspendedSize = args.suspended_size
    if args.IsKnownAndSpecified('stopped_size'):
      patch_instance_group_manager.targetStoppedSize = args.stopped_size

    resource_policies = managed_instance_groups_utils.CreateResourcePolicies(
        client.messages, args
    )
    patch_instance_group_manager.resourcePolicies = resource_policies

    if args.IsKnownAndSpecified('multi_mig'):
      if args.multi_mig:
        multi_mig_region = self._GetRegionForGroup(igm_ref)
        multi_mig_ref = holder.resources.Parse(
            args.multi_mig,
            params={'region': multi_mig_region, 'project': igm_ref.project},
            collection='compute.regionMultiMigs',
        )
        patch_instance_group_manager.multiMig = multi_mig_ref.SelfLink()
    elif args.IsKnownAndSpecified('clear_multi_mig'):
      patch_instance_group_manager.multiMig = None
      include_fields.append('multiMig')

    return patch_instance_group_manager, include_fields

  def Run(self, args):
    holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
    client = holder.client
    igm_ref = (
        instance_groups_flags.MULTISCOPE_INSTANCE_GROUP_MANAGER_ARG.ResolveAsResource
    )(
        args,
        holder.resources,
        default_scope=compute_scope.ScopeEnum.ZONE,
        scope_lister=flags.GetDefaultScopeLister(client),
    )

    if igm_ref:
      collection = (
          igm_ref[0].Collection()
          if isinstance(igm_ref, list)
          else igm_ref.Collection()
      )
      if collection not in [
          'compute.instanceGroupManagers',
          'compute.regionInstanceGroupManagers',
      ]:
        raise ValueError('Unknown reference type {0}'.format(collection))

    igm_resource = managed_instance_groups_utils.GetInstanceGroupManagerOrThrow(
        igm_ref, client
    )

    patch_instance_group_manager, include_fields = (
        self._CreateInstanceGroupManagerPatch(
            args, igm_ref, igm_resource, client, holder
        )
    )

    with client.apitools_client.IncludeFields(include_fields):
      return self._MakePatchRequest(
          client, igm_ref, patch_instance_group_manager
      )


UpdateGA.detailed_help = {
    'brief': 'Update a Compute Engine managed instance group.',
    'DESCRIPTION': """\
      Update a Compute Engine managed instance group.

      *{command}* allows you to specify or modify the description and group
      policies for an existing managed instance group, including the group's
      update policy and optional autohealing and stateful policies

      The group's update policy defines how an updated VM configuration is
      applied to existing VMs in the group. For more information, see
      [Applying new configurations]
      (https://cloud.google.com/compute/docs/instance-groups/updating-migs)
      to VMs in a MIG.

      A stateful policy defines which resources should be preserved across the
      group. When instances in the group are recreated, stateful resources are
      preserved. This command allows you to update stateful resources,
      specifically to add or remove stateful disks.

      When updating the autohealing policy, you can specify the health check,
      initial delay, or both. If either field is unspecified, its value won't
      be modified. If `--health-check` is specified, the health check monitors
      the health of your application. Whenever the health check signal for an
      instance becomes `UNHEALTHY`, the autohealer recreates the instance.

      If no health check exists, instance autohealing is triggered only by
      instance status: if an instance is not `RUNNING`, the group recreates it.
      """,
}


@base.ReleaseTracks(base.ReleaseTrack.BETA)
class UpdateBeta(UpdateGA):
  """Update a Compute Engine managed instance group."""

  support_update_policy_min_ready_flag = True
  support_multi_mig_flag = True
  support_instance_selection_min_cpu_platform = True
  support_flex_policy_flag_with_mincpu_and_disks = True

  @classmethod
  def Args(cls, parser):
    managed_flags.AddOnRepairFlags(parser)
    super(UpdateBeta, cls).Args(parser)

  def _CreateInstanceGroupManagerPatch(
      self, args, igm_ref, igm_resource, client, holder
  ):
    patch_instance_group_manager, include_fields = super(
        UpdateBeta, self
    )._CreateInstanceGroupManagerPatch(
        args, igm_ref, igm_resource, client, holder
    )
    return patch_instance_group_manager, include_fields


UpdateBeta.detailed_help = UpdateGA.detailed_help


@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class UpdateAlpha(UpdateBeta):
  """Update a Compute Engine managed instance group."""

  support_multi_mig_flag = True
  support_instance_selection_min_cpu_platform = True
  support_flex_policy_flag_with_mincpu_and_disks = True

  @classmethod
  def Args(cls, parser):
    super(UpdateAlpha, cls).Args(parser)

  def _CreateInstanceGroupManagerPatch(
      self, args, igm_ref, igm_resource, client, holder
  ):
    igm_patch, include_fields = super(
        UpdateAlpha, self
    )._CreateInstanceGroupManagerPatch(
        args, igm_ref, igm_resource, client, holder
    )

    return igm_patch, include_fields


UpdateAlpha.detailed_help = UpdateBeta.detailed_help
