# -*- 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.
"""Update a key."""


from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.cloudkms import base as cloudkms_base
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.kms import exceptions as kms_exceptions
from googlecloudsdk.command_lib.kms import flags
from googlecloudsdk.command_lib.kms import maps
from googlecloudsdk.command_lib.kms import resource_args
from googlecloudsdk.command_lib.util.args import labels_util


@base.UniverseCompatible
@base.ReleaseTracks(
    base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA, base.ReleaseTrack.GA
)
class Update(base.UpdateCommand):
  r"""Update a key.

  1. Update the rotation schedule for the given key.

  Updates the rotation schedule for the given key. The schedule
  automatically creates a new primary version for the key
  according to `next-rotation-time` and `rotation-period` flags.

  Flag `next-rotation-time` must be in ISO 8601 or RFC3339 format,
  and `rotation-period` must be in the form INTEGER[UNIT], where units
  can be one of seconds (s), minutes (m), hours (h) or days (d).

  Key rotations performed manually via `update-primary-version` and the
  version `create` do not affect the stored `next-rotation-time`.

  2. Remove the rotation schedule for the given key with
  `remove-rotation-schedule` flag.

  3. Update/Remove the labels for the given key with `update-labels` and/or
  `remove-labels` flags.

  4. Update the primary version for the given key with `primary-version` flag.

  5. Update the Key Access Justifications policy for the given key with
  `allowed-access-reasons` flag to allow specified reasons. The key must be
  enrolled in Key Access Justifications to use this flag.

  6. Remove the Key Access Justifications policy for the given key with
  `remove-key-access-justifications-policy` flag. The key must be enrolled in
  Key Access Justifications to use this flag.

  7. Update the Key Access Justifications policy for the given key with
  `allowed_access_reasons` flag to allow zero access reasons. This effectively
  disables the key, because a policy is configured to reject all access reasons.
  The key must be enrolled in Key Access Justifications to use this flag.

  ## EXAMPLES

  The following command sets a 30 day rotation period for the key
  named `frodo` within the keyring `fellowship` and location `global`
  starting at the specified time:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --rotation-period=30d \
        --next-rotation-time=2017-10-12T12:34:56.1234Z

  The following command removes the rotation schedule for the key
  named `frodo` within the keyring `fellowship` and location `global`:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --remove-rotation-schedule

  The following command updates the labels value for the key
  named `frodo` within the keyring `fellowship` and location `global`. If the
  label key does not exist at the time, it will be added:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --update-labels=k1=v1

  The following command removes labels k1 and k2 from the key
  named `frodo` within the keyring `fellowship` and location `global`:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --remove-labels=k1,k2

  The following command updates the primary version for the key
  named `frodo` within the keyring `fellowship` and location `global`:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --primary-version=1

  The following command updates the default algorithm for the key named `frodo`
  within the keyring `fellowship` and location `global`, assuming the key
  originally has purpose 'asymmetric-encryption' and algorithm
  'rsa-decrypt-oaep-2048-sha256':

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --default-algorithm=rsa-decrypt-oaep-4096-sha256

  The following command updates the Key Access Justifications policy for the key
  named `frodo` within the keyring ``fellowship'' and location ``global'' to
  allow only ``customer-initiated-access'' and
  ``google-initiated-system-operation'':

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --allowed-access-reasons=customer-initiated-access,google-initiated-system-operation

  The following command removes the Key Access Justifications policy for the key
  named `frodo` within the keyring ``fellowship'' and location ``global'', which
  results in all access reasons being allowed:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --remove-key-access-justifications-policy

  The following command updates the Key Access Justifications policy for the key
  named `frodo` within the keyring ``fellowship'' and location ``global'' to
  allow only zero access reasons, effectively disabling the key:

    $ {command} frodo \
        --location=global \
        --keyring=fellowship \
        --allowed-access-reasons=
  """

  @staticmethod
  def Args(parser):
    resource_args.AddKmsKeyResourceArgForKMS(parser, True, 'key')
    flags.AddRotationPeriodFlag(parser)
    flags.AddNextRotationTimeFlag(parser)
    flags.AddRemoveRotationScheduleFlag(parser)
    flags.AddCryptoKeyPrimaryVersionFlag(parser, 'to make primary')
    labels_util.AddUpdateLabelsFlags(parser)
    flags.AddDefaultAlgorithmFlag(parser)
    flags.AddAllowedAccessReasonsFlag(parser)
    flags.AddRemoveKeyAccessJustificationsPolicyFlag(parser)

  def ProcessFlags(self, args):
    fields_to_update = []

    labels_diff = labels_util.Diff.FromUpdateArgs(args)
    if labels_diff.MayHaveUpdates():
      fields_to_update.append('labels')
    if args.remove_rotation_schedule:
      if args.rotation_period or args.next_rotation_time:
        raise kms_exceptions.ArgumentError(
            'You cannot set and remove rotation schedule at the same time.')
      fields_to_update.append('rotationPeriod')
      fields_to_update.append('nextRotationTime')
    if args.rotation_period:
      fields_to_update.append('rotationPeriod')
    if args.next_rotation_time:
      fields_to_update.append('nextRotationTime')
    if args.default_algorithm:
      fields_to_update.append('versionTemplate.algorithm')
    if (
        args.allowed_access_reasons is not None
        and args.remove_key_access_justifications_policy
    ):
      raise kms_exceptions.ArgumentError(
          'You cannot set and remove a Key Access Justifications policy at the '
          'same time.'
      )
    if (
        args.allowed_access_reasons is not None
        or args.remove_key_access_justifications_policy
    ):
      fields_to_update.append('keyAccessJustificationsPolicy')

    # Raise an exception when no update field is specified.
    if not args.primary_version and not fields_to_update:
      raise kms_exceptions.UpdateError(
          'At least one of --primary-version or --update-labels or '
          '--remove-labels or --clear-labels or --rotation-period or '
          '--next-rotation-time or --remove-rotation-schedule or '
          '--default-algorithm or --allowed-access-reasons or '
          '--remove-key-access-justifications-policy must be specified.'
      )

    return fields_to_update

  def UpdatePrimaryVersion(self, args):
    client = cloudkms_base.GetClientInstance()
    messages = cloudkms_base.GetMessagesModule()
    crypto_key_ref = args.CONCEPTS.key.Parse()
    req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysUpdatePrimaryVersionRequest(  # pylint: disable=line-too-long
        name=crypto_key_ref.RelativeName(),
        updateCryptoKeyPrimaryVersionRequest=(
            messages.UpdateCryptoKeyPrimaryVersionRequest(
                cryptoKeyVersionId=args.primary_version)))

    try:
      response = client.projects_locations_keyRings_cryptoKeys.UpdatePrimaryVersion(  # pylint: disable=line-too-long
          req)
    except apitools_exceptions.HttpError:
      return None

    return response

  def UpdateOthers(self, args, crypto_key, fields_to_update):
    client = cloudkms_base.GetClientInstance()
    messages = cloudkms_base.GetMessagesModule()
    crypto_key_ref = args.CONCEPTS.key.Parse()

    labels_update = labels_util.Diff.FromUpdateArgs(args).Apply(
        messages.CryptoKey.LabelsValue, crypto_key.labels)

    if labels_update.needs_update:
      new_labels = labels_update.labels
    else:
      new_labels = crypto_key.labels

    req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysPatchRequest(
        name=crypto_key_ref.RelativeName(),
        cryptoKey=messages.CryptoKey(
            labels=new_labels))
    req.updateMask = ','.join(fields_to_update)
    flags.SetNextRotationTime(args, req.cryptoKey)
    flags.SetRotationPeriod(args, req.cryptoKey)
    if args.default_algorithm:
      valid_algorithms = maps.VALID_ALGORITHMS_MAP[crypto_key.purpose]
      if args.default_algorithm not in valid_algorithms:
        raise kms_exceptions.UpdateError(
            'Update failed: Algorithm {algorithm} is not valid. Here are the '
            'valid algorithm(s) for purpose {purpose}: {all_algorithms}'.format(
                algorithm=args.default_algorithm,
                purpose=crypto_key.purpose,
                all_algorithms=', '.join(valid_algorithms)))
      req.cryptoKey.versionTemplate = messages.CryptoKeyVersionTemplate(
          algorithm=maps.ALGORITHM_MAPPER.GetEnumForChoice(
              args.default_algorithm))
    if not args.remove_key_access_justifications_policy:
      flags.SetKeyAccessJustificationsPolicy(args, req.cryptoKey)

    try:
      response = client.projects_locations_keyRings_cryptoKeys.Patch(req)
    except apitools_exceptions.HttpError:
      return None

    return response

  def HandleErrors(self, args, set_primary_version_succeeds,
                   other_updates_succeed, fields_to_update):
    """Handles various errors that may occur during any update stage.

    Never returns without an exception.

    Args:
      args: Input arguments.
      set_primary_version_succeeds: True if the primary verion is updated
        successfully.
      other_updates_succeed: True if all other updates (besides primary verions)
        is updated successfully.
      fields_to_update: A list of fields to be updated.

    Raises:
      ToolException: An exception raised when there is error during any update
      stage.
    """
    err = 'An Error occurred:'
    if not set_primary_version_succeeds:
      err += " Failed to update field 'primaryVersion'."
    elif args.primary_version:
      err += " Field 'primaryVersion' was updated."
    if not other_updates_succeed:
      err += " Failed to update field(s) '{}'.".format(
          "', '".join(fields_to_update))
    elif fields_to_update:
      err += " Field(s) '{}' were updated.".format(
          "', '".join(fields_to_update))
    raise kms_exceptions.UpdateError(err)

  def Run(self, args):
    """Updates the relevant fields (of a CryptoKey) from the flags."""

    # Check the flags and raise an exception if any check fails.
    fields_to_update = self.ProcessFlags(args)

    # Try to get the cryptoKey and raise an exception if the key doesn't exist.
    client = cloudkms_base.GetClientInstance()
    messages = cloudkms_base.GetMessagesModule()
    crypto_key_ref = args.CONCEPTS.key.Parse()
    crypto_key = client.projects_locations_keyRings_cryptoKeys.Get(
        messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysGetRequest(
            name=crypto_key_ref.RelativeName()))

    # Try to update the key's primary version.
    set_primary_version_succeeds = True
    if args.primary_version:
      response = self.UpdatePrimaryVersion(args)
      if response:
        crypto_key = response  # If call succeeds, update the crypto_key.
      else:
        set_primary_version_succeeds = False

    # Try other updates.
    other_updates_succeed = True
    if fields_to_update:
      response = self.UpdateOthers(args, crypto_key, fields_to_update)
      if response:
        crypto_key = response  # If call succeeds, update the crypto_key.
      else:
        other_updates_succeed = False

    if not set_primary_version_succeeds or not other_updates_succeed:
      self.HandleErrors(args, set_primary_version_succeeds,
                        other_updates_succeed, fields_to_update)
    else:
      return crypto_key
