# -*- coding: utf-8 -*- #
# Copyright 2014 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 modifying backend services."""

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

import io
from apitools.base.protorpclite import messages
from apitools.base.py import encoding

from googlecloudsdk.api_lib.compute import base_classes
from googlecloudsdk.api_lib.compute import property_selector
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.compute import flags as compute_flags
from googlecloudsdk.command_lib.compute.backend_services import backend_services_utils
from googlecloudsdk.command_lib.compute.backend_services import flags
from googlecloudsdk.core import resources
from googlecloudsdk.core import yaml
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import edit
import six


class InvalidResourceError(calliope_exceptions.ToolException):
  # Normally we'd want to subclass core.exceptions.Error, but base_classes.Edit
  # abuses ToolException to classify errors when displaying messages to users,
  # and we should continue to fit in that framework for now.
  pass


class Edit(base.Command):
  """Modify backend services.

    *{command}* can be used to modify a backend service. The backend
  service resource is fetched from the server and presented in a text
  editor. After the file is saved and closed, this command will
  update the resource. Only fields that can be modified are
  displayed in the editor.

  Backends are named by their associated instances groups, and one
  of the ``--group'' or ``--instance-group'' flags is required to
  identify the backend that you are modifying.  You cannot "change"
  the instance group associated with a backend, but you can accomplish
  something similar with ``backend-services remove-backend'' and
  ``backend-services add-backend''.

  The editor used to modify the resource is chosen by inspecting
  the ``EDITOR'' environment variable.
  """

  DEFAULT_FORMAT = 'yaml'
  _BACKEND_SERVICE_ARG = flags.GLOBAL_REGIONAL_BACKEND_SERVICE_ARG

  @classmethod
  def Args(cls, parser):
    cls._BACKEND_SERVICE_ARG.AddArgument(parser)

  def _ProcessEditedResource(self, holder, backend_service_ref, file_contents,
                             original_object, original_record,
                             modifiable_record, args):
    """Returns an updated resource that was edited by the user."""

    # It's very important that we replace the characters of comment
    # lines with spaces instead of removing the comment lines
    # entirely. JSON and YAML deserialization give error messages
    # containing line, column, and the character offset of where the
    # error occurred. If the deserialization fails; we want to make
    # sure those numbers map back to what the user actually had in
    # front of him or her otherwise the errors will not be very
    # useful.
    non_comment_lines = '\n'.join(
        ' ' * len(line) if line.startswith('#') else line
        for line in file_contents.splitlines())

    modified_record = base_classes.DeserializeValue(
        non_comment_lines, args.format or Edit.DEFAULT_FORMAT)

    # Normalizes all of the fields that refer to other
    # resource. (i.e., translates short names to URIs)
    reference_normalizer = property_selector.PropertySelector(
        transformations=self.GetReferenceNormalizers(holder.resources))
    modified_record = reference_normalizer.Apply(modified_record)

    if modifiable_record == modified_record:
      new_object = None

    else:
      modified_record['name'] = original_record['name']
      fingerprint = original_record.get('fingerprint')
      if fingerprint:
        modified_record['fingerprint'] = fingerprint

      new_object = encoding.DictToMessage(modified_record,
                                          holder.client.messages.BackendService)

    # If existing object is equal to the proposed object or if
    # there is no new object, then there is no work to be done, so we
    # return the original object.
    if not new_object or original_object == new_object:
      return [original_object]

    return holder.client.MakeRequests(
        [self.GetSetRequest(holder.client, backend_service_ref, new_object)])

  def Run(self, args):
    holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
    client = holder.client

    backend_service_ref = self._BACKEND_SERVICE_ARG.ResolveAsResource(
        args,
        holder.resources,
        default_scope=backend_services_utils.GetDefaultScope(),
        scope_lister=compute_flags.GetDefaultScopeLister(client))
    get_request = self.GetGetRequest(client, backend_service_ref)

    objects = client.MakeRequests([get_request])

    original_object = objects[0]
    original_record = encoding.MessageToDict(original_object)

    # Selects only the fields that can be modified.
    field_selector = property_selector.PropertySelector(properties=[
        'backends',
        'description',
        'enableCDN',
        'healthChecks',
        'iap.enabled',
        'iap.oauth2ClientId',
        'iap.oauth2ClientSecret',
        'port',
        'portName',
        'protocol',
        'timeoutSec',
    ])
    modifiable_record = field_selector.Apply(original_record)

    file_contents = self.BuildFileContents(args, client, original_record,
                                           modifiable_record)
    resource_list = self.EditResource(args, backend_service_ref, file_contents,
                                      holder, modifiable_record,
                                      original_object, original_record)

    for resource in resource_list:
      yield resource

  def BuildFileContents(self, args, client, original_record, modifiable_record):
    buf = io.StringIO()
    for line in base_classes.HELP.splitlines():
      buf.write('#')
      if line:
        buf.write(' ')
      buf.write(line)
      buf.write('\n')
    buf.write('\n')
    buf.write(base_classes.SerializeDict(modifiable_record,
                                         args.format or Edit.DEFAULT_FORMAT))
    buf.write('\n')
    example = base_classes.SerializeDict(
        encoding.MessageToDict(self.GetExampleResource(client)),
        args.format or Edit.DEFAULT_FORMAT)
    base_classes.WriteResourceInCommentBlock(example, 'Example resource:', buf)
    buf.write('#\n')
    original = base_classes.SerializeDict(original_record,
                                          args.format or Edit.DEFAULT_FORMAT)
    base_classes.WriteResourceInCommentBlock(original, 'Original resource:',
                                             buf)
    return buf.getvalue()

  def EditResource(self, args, backend_service_ref, file_contents, holder,
                   modifiable_record, original_object, original_record):
    while True:
      try:
        file_contents = edit.OnlineEdit(file_contents)
      except edit.NoSaveException:
        raise calliope_exceptions.ToolException('Edit aborted by user.')
      try:
        resource_list = self._ProcessEditedResource(holder, backend_service_ref,
                                                    file_contents,
                                                    original_object,
                                                    original_record,
                                                    modifiable_record, args)
        break
      except (ValueError, yaml.YAMLParseError,
              messages.ValidationError,
              calliope_exceptions.ToolException) as e:
        message = getattr(e, 'message', six.text_type(e))

        if isinstance(e, calliope_exceptions.ToolException):
          problem_type = 'applying'
        else:
          problem_type = 'parsing'

        message = ('There was a problem {0} your changes: {1}'
                   .format(problem_type, message))
        if not console_io.PromptContinue(
            message=message,
            prompt_string='Would you like to edit the resource again?'):
          raise calliope_exceptions.ToolException('Edit aborted by user.')
    return resource_list

  def GetExampleResource(self, client):
    uri_prefix = ('https://compute.googleapis.com/compute/v1/projects/'
                  'my-project/')
    instance_groups_uri_prefix = (
        'https://compute.googleapis.com/compute/v1/projects/'
        'my-project/zones/')

    return client.messages.BackendService(
        backends=[
            client.messages.Backend(
                balancingMode=(
                    client.messages.Backend.BalancingModeValueValuesEnum.RATE),
                group=(
                    instance_groups_uri_prefix +
                    'us-central1-a/instanceGroups/group-1'),
                maxRate=100),
            client.messages.Backend(
                balancingMode=(
                    client.messages.Backend.BalancingModeValueValuesEnum.RATE),
                group=(
                    instance_groups_uri_prefix +
                    'europe-west1-a/instanceGroups/group-2'),
                maxRate=150),
        ],
        description='My backend service',
        healthChecks=[
            uri_prefix + 'global/httpHealthChecks/my-health-check-1',
            uri_prefix + 'global/httpHealthChecks/my-health-check-2'
        ],
        name='backend-service',
        port=80,
        portName='http',
        protocol=client.messages.BackendService.ProtocolValueValuesEnum.HTTP,
        selfLink=uri_prefix + 'global/backendServices/backend-service',
        timeoutSec=30,
    )

  def GetReferenceNormalizers(self, resource_registry):

    def MakeReferenceNormalizer(field_name, allowed_collections):
      """Returns a function to normalize resource references."""
      def NormalizeReference(reference):
        """Returns normalized URI for field_name."""
        try:
          value_ref = resource_registry.Parse(reference)
        except resources.UnknownCollectionException:
          raise InvalidResourceError(
              '[{field_name}] must be referenced using URIs.'.format(
                  field_name=field_name))

        if value_ref.Collection() not in allowed_collections:
          raise InvalidResourceError(
              'Invalid [{field_name}] reference: [{value}].'. format(
                  field_name=field_name, value=reference))
        return value_ref.SelfLink()
      return NormalizeReference

    # Ensure group is a uri or full collection path representing an instance
    # group. Full uris/paths are required because if the user gives us less, we
    # don't want to be in the business of guessing health checks.
    return [
        ('healthChecks[]',
         MakeReferenceNormalizer(
             'healthChecks',
             ('compute.httpHealthChecks', 'compute.httpsHealthChecks',
              'compute.healthChecks', 'compute.regionHealthChecks'))),
        ('backends[].group',
         MakeReferenceNormalizer(
             'group',
             ('compute.instanceGroups', 'compute.regionInstanceGroups'))),
    ]

  def GetGetRequest(self, client, backend_service_ref):
    if backend_service_ref.Collection() == 'compute.regionBackendServices':
      return (client.apitools_client.regionBackendServices, 'Get',
              client.messages.ComputeRegionBackendServicesGetRequest(
                  **backend_service_ref.AsDict()))
    return (client.apitools_client.backendServices, 'Get',
            client.messages.ComputeBackendServicesGetRequest(
                **backend_service_ref.AsDict()))

  def GetSetRequest(self, client, backend_service_ref, replacement):
    if backend_service_ref.Collection() == 'compute.regionBackendServices':
      return (client.apitools_client.regionBackendServices, 'Update',
              client.messages.ComputeRegionBackendServicesUpdateRequest(
                  backendServiceResource=replacement,
                  **backend_service_ref.AsDict()))
    return (client.apitools_client.backendServices, 'Update',
            client.messages.ComputeBackendServicesUpdateRequest(
                backendServiceResource=replacement,
                **backend_service_ref.AsDict()))
