# -*- coding: utf-8 -*- #
# Copyright 2020 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 creating instances."""


from googlecloudsdk.api_lib.compute import base_classes
from googlecloudsdk.api_lib.compute import filter_rewrite
from googlecloudsdk.api_lib.compute import utils
from googlecloudsdk.api_lib.compute.regions import utils as region_utils
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.compute import scope as compute_scopes
from googlecloudsdk.command_lib.compute.instances import flags as instances_flags
from googlecloudsdk.command_lib.compute.instances.bulk import flags as bulk_flags
from googlecloudsdk.command_lib.compute.instances.bulk import util as bulk_util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties

DETAILED_HELP = {
    'brief': """
          Create multiple Compute Engine virtual machines.
        """,
    'DESCRIPTION': """
        *{command}* facilitates the creation of multiple Compute Engine
        virtual machines with a single command. They offer a number of advantages
        compared to the single instance creation command. This includes the
        ability to automatically pick a zone in which to create instances based
        on resource availability, the ability to specify that the request be
        atomic or best-effort, and a faster rate of instance creation.
        """,
    'EXAMPLES': """
        To create instances called 'example-instance-1', 'example-instance-2',
        and 'example-instance-3' in the 'us-central1-a' zone, run:

          $ {command} --predefined-names=example-instance-1,example-instance-2,example-instance-3 --zone=us-central1-a
        """,
}


def _GetOperations(
    compute_client, project, operation_group_id, holder, location, scope
):
  """Requests operations with group id matching the given one."""

  errors_to_collect = []

  _, operation_filter = filter_rewrite.Rewriter().Rewrite(
      expression='operationGroupId=' + operation_group_id
  )

  resource_parser = holder.resources
  zones = []
  if scope == compute_scopes.ScopeEnum.REGION:
    region_fetcher = region_utils.RegionResourceFetcher(holder.client)
    regions = region_fetcher.GetRegions([
        resource_parser.Create(
            collection='compute.regions', project=project, region=location
        )
    ])
    if len(regions) != 1:
      errors_to_collect.append(
          exceptions.ToolException('Region count is not 1: {}'.format(location))
      )
      return None, errors_to_collect
    zones += [resource_parser.Parse(zone).zone for zone in regions[0].zones]
  else:
    zones += [location]

  operations_response = compute_client.MakeRequests(
      [
          (
              compute_client.apitools_client.zoneOperations,
              'List',
              compute_client.apitools_client.zoneOperations.GetRequestType(
                  'List'
              )(filter=operation_filter, project=project, zone=zone),
          )
          for zone in zones
      ],
      errors_to_collect=errors_to_collect,
      log_result=False,
      always_return_operation=True,
      no_followup=True,
  )

  return operations_response, errors_to_collect


def _GetResult(
    compute_client, request, operation_group_id, holder, location, scope
):
  """Requests operations with group id and parses them as an output."""

  operations_response, errors = _GetOperations(
      compute_client,
      request.project,
      operation_group_id,
      holder,
      location,
      scope,
  )
  if errors:
    utils.RaiseToolException(errors, error_message='Could not fetch resource:')
  result = {'operationGroupId': operation_group_id, 'instances': []}
  successful = [
      op
      for op in operations_response
      if op.operationType == 'insert'
      and str(op.status) == 'DONE'
      and op.error is None
  ]
  num_successful = len(successful)
  num_unsuccessful = request.bulkInsertInstanceResource.count - num_successful

  def GetInstanceStatus(op):
    return {
        'id': op.targetId,
        'name': op.targetLink.split('/')[-1],
        'zone': op.zone,
        'selfLink': op.targetLink,
    }

  instances_status = [GetInstanceStatus(op) for op in successful]

  result['createdInstanceCount'] = num_successful
  result['failedInstanceCount'] = num_unsuccessful
  result['instances'] = instances_status

  return result


@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA, base.ReleaseTrack.PREVIEW)
class Create(base.Command):
  """Create Compute Engine virtual machine instances."""

  _support_display_device = False
  _support_secure_tags = False
  _support_numa_node_count = False
  _support_snp_svsm = False
  _support_max_count_per_zone = True
  _support_custom_hostnames = False
  _support_specific_then_x_affinity = False
  _support_any_reservation_then_fail_affinity = False
  _support_watchdog_timer = False
  _support_graceful_shutdown = True
  _support_flex_start = False
  _support_source_snapshot_region = False
  _support_skip_guest_os_shutdown = True
  _support_preemption_notice_duration = False
  _support_instance_flexibility_policy = True
  _support_workload_identity_config = False
  _support_instance_selection_min_cpu_platform = False
  _support_vsock_mode = False

  _log_async = False

  @classmethod
  def Args(cls, parser):
    bulk_flags.AddCommonBulkInsertArgs(
        parser,
        base.ReleaseTrack.GA,
        support_display_device=cls._support_display_device,
        support_numa_node_count=cls._support_numa_node_count,
        support_snp_svsm=cls._support_snp_svsm,
        support_max_count_per_zone=cls._support_max_count_per_zone,
        support_custom_hostnames=cls._support_custom_hostnames,
        support_specific_then_x_affinity=cls._support_specific_then_x_affinity,
        support_any_reservation_then_fail_affinity=cls._support_any_reservation_then_fail_affinity,
        support_watchdog_timer=cls._support_watchdog_timer,
        support_flex_start=cls._support_flex_start,
        support_source_snapshot_region=cls._support_source_snapshot_region,
        support_skip_guest_os_shutdown=cls._support_skip_guest_os_shutdown,
        support_preemption_notice_duration=cls._support_preemption_notice_duration,
        support_instance_flexibility_policy=cls._support_instance_flexibility_policy,
        support_workload_identity_config=cls._support_workload_identity_config,
        support_instance_selection_min_cpu_platform=cls._support_instance_selection_min_cpu_platform,
        support_vsock_mode=cls._support_vsock_mode,
    )
    cls.AddSourceInstanceTemplate(parser)

    # Flags specific to GA release track
    instances_flags.AddLocalSsdRecoveryTimeoutArgs(parser)
    instances_flags.AddHostErrorTimeoutSecondsArgs(parser)

  # LINT.IfChange(instance_template)
  @classmethod
  def AddSourceInstanceTemplate(cls, parser):
    cls.SOURCE_INSTANCE_TEMPLATE = (
        bulk_flags.MakeBulkSourceInstanceTemplateArg()
    )
    cls.SOURCE_INSTANCE_TEMPLATE.AddArgument(parser)

  # LINT.ThenChange(../../queued_resources/create.py:instance_template)

  def Collection(self):
    return 'compute.instances'

  def _CreateRequests(
      self,
      args,
      holder,
      compute_client,
      resource_parser,
      project,
      location,
      scope,
  ):
    supported_features = bulk_util.SupportedFeatures(
        self._support_display_device,
        self._support_secure_tags,
        self._support_numa_node_count,
        self._support_snp_svsm,
        self._support_max_count_per_zone,
        self._support_custom_hostnames,
        self._support_specific_then_x_affinity,
        self._support_any_reservation_then_fail_affinity,
        self._support_watchdog_timer,
        self._support_graceful_shutdown,
        self._support_source_snapshot_region,
        self._support_skip_guest_os_shutdown,
        self._support_preemption_notice_duration,
        self._support_instance_flexibility_policy,
        self._support_workload_identity_config,
        self._support_instance_selection_min_cpu_platform,
        support_vsock_mode=self._support_vsock_mode,
    )
    bulk_instance_resource = bulk_util.CreateBulkInsertInstanceResource(
        args,
        holder,
        compute_client,
        resource_parser,
        project,
        location,
        scope,
        self.SOURCE_INSTANCE_TEMPLATE,
        supported_features,
    )

    if scope == compute_scopes.ScopeEnum.ZONE:
      instance_service = compute_client.apitools_client.instances
      request_message = (
          compute_client.messages.ComputeInstancesBulkInsertRequest(
              bulkInsertInstanceResource=bulk_instance_resource,
              project=project,
              zone=location,
          )
      )
    elif scope == compute_scopes.ScopeEnum.REGION:
      instance_service = compute_client.apitools_client.regionInstances
      request_message = (
          compute_client.messages.ComputeRegionInstancesBulkInsertRequest(
              bulkInsertInstanceResource=bulk_instance_resource,
              project=project,
              region=location,
          )
      )

    return instance_service, request_message

  def Run(self, args):
    """Runs bulk create command.

    Args:
      args: argparse.Namespace, An object that contains the values for the
        arguments specified in the .Args() method.

    Returns:
      A resource object dispatched by display.Displayer().
    """
    bulk_flags.ValidateBulkInsertArgs(
        args,
        support_max_count_per_zone=self._support_max_count_per_zone,
        support_custom_hostnames=self._support_custom_hostnames,
        support_instance_flexibility_policy=self._support_instance_flexibility_policy,
    )

    holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
    compute_client = holder.client
    resource_parser = holder.resources

    project = properties.VALUES.core.project.GetOrFail()
    location = None
    scope = None

    if args.IsSpecified('zone'):
      location = args.zone
      scope = compute_scopes.ScopeEnum.ZONE
    elif args.IsSpecified('region'):
      location = args.region
      scope = compute_scopes.ScopeEnum.REGION

    instances_service, request = self._CreateRequests(
        args, holder, compute_client, resource_parser, project, location, scope
    )

    self._errors = []
    self._log_async = False
    self._status_message = None

    if args.async_:
      self._log_async = True
      try:
        response = instances_service.BulkInsert(request)
        self._operation_selflink = response.selfLink
        return {'operationGroupId': response.operationGroupId}
      except exceptions.HttpException as error:
        raise error

    response = compute_client.MakeRequests(
        [(instances_service, 'BulkInsert', request)],
        log_result=False,
        always_return_operation=True,
        no_followup=True,
    )

    self._errors = []
    if response:
      operation_group_id = response[0].operationGroupId
      result = _GetResult(
          compute_client, request, operation_group_id, holder, location, scope
      )
      if (
          result.get('createdInstanceCount') is not None
          and result.get('failedInstanceCount') is not None
      ):
        self._status_message = 'VM instances created: {}, failed: {}.'.format(
            result['createdInstanceCount'], result['failedInstanceCount']
        )
      return result
    return

  def Epilog(self, resources_were_displayed):
    del resources_were_displayed
    if self._errors:
      for error in self._errors:
        log.error(error[1])
    elif self._log_async:
      log.status.Print(
          'Bulk instance creation in progress: {}'.format(
              self._operation_selflink
          )
      )
    else:
      if self._errors:
        for error in self._errors:
          log.warning(error[1])
      log.status.Print(
          'Bulk create request finished with status message: [{}]'.format(
              self._status_message
          )
      )


@base.ReleaseTracks(base.ReleaseTrack.BETA)
class CreateBeta(Create):
  """Create Compute Engine virtual machine instances."""

  _support_display_device = True
  _support_secure_tags = False
  _support_numa_node_count = False
  _support_snp_svsm = False
  _support_max_count_per_zone = True
  _support_custom_hostnames = True
  _support_specific_then_x_affinity = True
  _support_any_reservation_then_fail_affinity = True
  _support_watchdog_timer = False
  _support_graceful_shutdown = True
  _support_flex_start = False
  _support_igmp_query = False
  _support_source_snapshot_region = False
  _support_skip_guest_os_shutdown = True
  _support_preemption_notice_duration = True
  _support_instance_flexibility_policy = True
  _support_workload_identity_config = False
  _support_instance_selection_min_cpu_platform = False

  @classmethod
  def Args(cls, parser):
    bulk_flags.AddCommonBulkInsertArgs(
        parser,
        base.ReleaseTrack.BETA,
        support_display_device=cls._support_display_device,
        support_numa_node_count=cls._support_numa_node_count,
        support_snp_svsm=cls._support_snp_svsm,
        support_max_count_per_zone=cls._support_max_count_per_zone,
        support_custom_hostnames=cls._support_custom_hostnames,
        support_specific_then_x_affinity=cls._support_specific_then_x_affinity,
        support_any_reservation_then_fail_affinity=cls._support_any_reservation_then_fail_affinity,
        support_watchdog_timer=cls._support_watchdog_timer,
        support_graceful_shutdown=cls._support_graceful_shutdown,
        support_flex_start=cls._support_flex_start,
        support_igmp_query=cls._support_igmp_query,
        support_source_snapshot_region=cls._support_source_snapshot_region,
        support_skip_guest_os_shutdown=cls._support_skip_guest_os_shutdown,
        support_preemption_notice_duration=cls._support_preemption_notice_duration,
        support_instance_flexibility_policy=cls._support_instance_flexibility_policy,
        support_workload_identity_config=cls._support_workload_identity_config,
        support_instance_selection_min_cpu_platform=cls._support_instance_selection_min_cpu_platform,
        support_vsock_mode=cls._support_vsock_mode,
    )
    cls.AddSourceInstanceTemplate(parser)

    # Flags specific to Beta release track
    instances_flags.AddHostErrorTimeoutSecondsArgs(parser)
    instances_flags.AddLocalSsdRecoveryTimeoutArgs(parser)


@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class CreateAlpha(Create):
  """Create Compute Engine virtual machine instances."""

  # LINT.IfChange(alpha_spec)
  _support_display_device = True
  _support_secure_tags = True
  _support_numa_node_count = True
  _support_snp_svsm = True
  _support_max_count_per_zone = True
  _support_custom_hostnames = True
  _support_specific_then_x_affinity = True
  _support_any_reservation_then_fail_affinity = True
  _support_watchdog_timer = True
  _support_igmp_query = True
  _support_graceful_shutdown = True
  _support_flex_start = False
  _support_source_snapshot_region = True
  _support_skip_guest_os_shutdown = True
  _support_preemption_notice_duration = True
  _support_instance_flexibility_policy = True
  _support_workload_identity_config = True
  _support_instance_selection_min_cpu_platform = True
  _support_vsock_mode = True

  @classmethod
  def Args(cls, parser):
    bulk_flags.AddCommonBulkInsertArgs(
        parser,
        base.ReleaseTrack.ALPHA,
        support_display_device=cls._support_display_device,
        support_numa_node_count=cls._support_numa_node_count,
        support_snp_svsm=cls._support_snp_svsm,
        support_max_count_per_zone=cls._support_max_count_per_zone,
        support_custom_hostnames=cls._support_custom_hostnames,
        support_specific_then_x_affinity=cls._support_specific_then_x_affinity,
        support_any_reservation_then_fail_affinity=cls._support_any_reservation_then_fail_affinity,
        support_watchdog_timer=cls._support_watchdog_timer,
        support_igmp_query=cls._support_igmp_query,
        support_graceful_shutdown=cls._support_graceful_shutdown,
        support_flex_start=cls._support_flex_start,
        support_source_snapshot_region=cls._support_source_snapshot_region,
        support_skip_guest_os_shutdown=cls._support_skip_guest_os_shutdown,
        support_preemption_notice_duration=cls._support_preemption_notice_duration,
        support_instance_flexibility_policy=cls._support_instance_flexibility_policy,
        support_workload_identity_config=cls._support_workload_identity_config,
        support_instance_selection_min_cpu_platform=cls._support_instance_selection_min_cpu_platform,
        support_vsock_mode=cls._support_vsock_mode,
    )

    cls.AddSourceInstanceTemplate(parser)

    # Flags specific to Alpha release track
    instances_flags.AddSecureTagsArgs(parser)
    instances_flags.AddHostErrorTimeoutSecondsArgs(parser)
    instances_flags.AddMaintenanceInterval().AddToParser(parser)
    instances_flags.AddLocalSsdRecoveryTimeoutArgs(parser)

  # LINT.ThenChange(../../queued_resources/create.py:alpha_spec)


Create.detailed_help = DETAILED_HELP
