# -*- 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 updating firewall rules."""


from apitools.base.py import encoding

from googlecloudsdk.api_lib.compute import base_classes
from googlecloudsdk.api_lib.compute import firewalls_utils
from googlecloudsdk.api_lib.compute import utils as compute_api
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.compute import exceptions
from googlecloudsdk.command_lib.compute.firewall_rules import flags


@base.ReleaseTracks(base.ReleaseTrack.GA, base.ReleaseTrack.PREVIEW)
class UpdateFirewall(base.UpdateCommand):
  """Update a firewall rule."""

  with_egress_firewall = True
  with_service_account = True

  FIREWALL_RULE_ARG = None

  @classmethod
  def Args(cls, parser):
    messages = apis.GetMessagesModule('compute',
                                      compute_api.COMPUTE_GA_API_VERSION)
    cls.FIREWALL_RULE_ARG = flags.FirewallRuleArgument()
    cls.FIREWALL_RULE_ARG.AddArgument(parser, operation_type='update')
    firewalls_utils.AddCommonArgs(
        parser,
        for_update=True,
        with_egress_support=cls.with_egress_firewall,
        with_service_account=cls.with_service_account)
    firewalls_utils.AddArgsForServiceAccount(parser, for_update=True)
    flags.AddEnableLogging(parser)
    flags.AddLoggingMetadata(parser, messages)

  def ValidateArgument(self, messages, args):
    self.new_allowed = firewalls_utils.ParseRules(
        args.allow, messages, firewalls_utils.ActionType.ALLOW)

    args_unset = all(
        x is None
        for x in (args.allow, args.description, args.source_ranges,
                  args.source_tags, args.target_tags))
    if self.with_egress_firewall:
      args_unset = args_unset and all(
          x is None
          for x in (args.destination_ranges, args.priority, args.rules))
    if self.with_service_account:
      args_unset = args_unset and all(
          x is None
          for x in (args.source_service_accounts, args.target_service_accounts))
    args_unset = args_unset and args.disabled is None
    args_unset = (args_unset and args.enable_logging is None)
    args_unset = args_unset and not args.logging_metadata
    if args_unset:
      raise exceptions.UpdatePropertyError(
          'At least one property must be modified.')

    if args.rules and args.allow:
      raise firewalls_utils.ArgumentValidationError(
          'Can NOT specify --rules and --allow in the same request.')

  def Run(self, args):
    """Issues requests necessary to update the Firewall rules."""
    holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
    client = holder.client

    self.ValidateArgument(client.messages, args)
    # Set the resource reference which is used in composing resource-get
    # request.
    resource_reference = self.FIREWALL_RULE_ARG.ResolveAsResource(
        args, holder.resources)
    get_request = self._GetGetRequest(client, resource_reference)
    cleared_fields = []
    objects = client.MakeRequests([get_request])

    new_object = self.Modify(client, args, objects[0], cleared_fields)

    # If existing object is equal to the proposed object or if
    # Modify() returns None, then there is no work to be done, so we
    # print the resource and exit.
    if not new_object or objects[0] == new_object:
      return objects[0]

    with client.apitools_client.IncludeFields(cleared_fields):
      resource_list = client.MakeRequests(
          [self._GetSetRequest(client, resource_reference, new_object)])

    return resource_list

  def _GetGetRequest(self, client, resource_reference):
    """Returns the request for the existing Firewall resource."""
    return (client.apitools_client.firewalls, 'Get',
            client.messages.ComputeFirewallsGetRequest(
                firewall=resource_reference.Name(),
                project=resource_reference.project))

  def _GetSetRequest(self, client, resource_reference, replacement):
    request = client.messages.ComputeFirewallsPatchRequest(
        firewall=replacement.name,
        firewallResource=replacement,
        project=resource_reference.project)
    return (client.apitools_client.firewalls, 'Patch', request)

  def _AddLoggingMetadata(self, messages, args, log_config):
    if args.IsSpecified('logging_metadata'):
      if log_config is None or not log_config.enable:
        raise calliope_exceptions.InvalidArgumentException(
            '--logging-metadata',
            'cannot toggle logging metadata if logging is not enabled.')

      log_config.metadata = flags.GetLoggingMetadataArg(
          messages).GetEnumForChoice(args.logging_metadata)

  def Modify(self, client, args, existing, cleared_fields):
    """Returns a modified Firewall message and included fields."""
    if args.allow:
      allowed = self.new_allowed
    elif args.allow is None:
      allowed = existing.allowed
    else:
      cleared_fields.append('allowed')
      allowed = []

    if args.description:
      description = args.description
    elif args.description is None:
      description = existing.description
    else:
      cleared_fields.append('description')
      description = None

    if args.source_ranges:
      source_ranges = args.source_ranges
    elif args.source_ranges is None:
      source_ranges = existing.sourceRanges
    else:
      cleared_fields.append('sourceRanges')
      source_ranges = []

    if args.source_tags:
      source_tags = args.source_tags
    elif args.source_tags is None:
      source_tags = existing.sourceTags
    else:
      cleared_fields.append('sourceTags')
      source_tags = []

    if args.target_tags:
      target_tags = args.target_tags
    elif args.target_tags is None:
      target_tags = existing.targetTags
    else:
      cleared_fields.append('targetTags')
      target_tags = []

    denied = []
    if args.rules:
      if existing.allowed:
        allowed = firewalls_utils.ParseRules(args.rules, client.messages,
                                             firewalls_utils.ActionType.ALLOW)
      else:
        denied = firewalls_utils.ParseRules(args.rules, client.messages,
                                            firewalls_utils.ActionType.DENY)
    elif args.rules is not None:
      if existing.allowed:
        cleared_fields.append('allowed')
        allowed = []
      else:
        cleared_fields.append('denied')
        denied = []

    direction = existing.direction

    if args.priority is None:
      priority = existing.priority
    else:
      priority = args.priority

    destination_ranges = []
    if args.destination_ranges:
      destination_ranges = args.destination_ranges
    elif args.destination_ranges is None:
      destination_ranges = existing.destinationRanges
    else:
      cleared_fields.append('destinationRanges')

    source_service_accounts = []
    if args.source_service_accounts:
      source_service_accounts = args.source_service_accounts
    elif args.source_service_accounts is None:
      source_service_accounts = existing.sourceServiceAccounts
    else:
      cleared_fields.append('sourceServiceAccounts')

    target_service_accounts = []
    if args.target_service_accounts:
      target_service_accounts = args.target_service_accounts
    elif args.target_service_accounts is None:
      target_service_accounts = existing.targetServiceAccounts
    else:
      cleared_fields.append('targetServiceAccounts')

    if args.IsSpecified('enable_logging'):
      log_config = client.messages.FirewallLogConfig(enable=args.enable_logging)
      self._AddLoggingMetadata(client.messages, args, log_config)
    else:
      log_config = (
          encoding.CopyProtoMessage(existing.logConfig)
          if existing.logConfig is not None else None)
      self._AddLoggingMetadata(client.messages, args, log_config)

    new_firewall = client.messages.Firewall(
        name=existing.name,
        direction=direction,
        priority=priority,
        allowed=allowed,
        denied=denied,
        description=description,
        network=existing.network,
        sourceRanges=source_ranges,
        sourceTags=source_tags,
        destinationRanges=destination_ranges,
        targetTags=target_tags,
        sourceServiceAccounts=source_service_accounts,
        targetServiceAccounts=target_service_accounts,
        logConfig=log_config)

    if args.disabled is not None:
      new_firewall.disabled = args.disabled

    return new_firewall


@base.ReleaseTracks(base.ReleaseTrack.BETA)
class BetaUpdateFirewall(UpdateFirewall):
  """Update a firewall rule."""

  @classmethod
  def Args(cls, parser):
    messages = apis.GetMessagesModule('compute',
                                      compute_api.COMPUTE_BETA_API_VERSION)
    cls.FIREWALL_RULE_ARG = flags.FirewallRuleArgument()
    cls.FIREWALL_RULE_ARG.AddArgument(parser, operation_type='update')
    firewalls_utils.AddCommonArgs(
        parser,
        for_update=True,
        with_egress_support=cls.with_egress_firewall,
        with_service_account=cls.with_service_account)
    firewalls_utils.AddArgsForServiceAccount(parser, for_update=True)
    flags.AddEnableLogging(parser)
    flags.AddLoggingMetadata(parser, messages)


@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class AlphaUpdateFirewall(BetaUpdateFirewall):
  """Update a firewall rule."""

  @classmethod
  def Args(cls, parser):
    messages = apis.GetMessagesModule('compute',
                                      compute_api.COMPUTE_ALPHA_API_VERSION)
    cls.FIREWALL_RULE_ARG = flags.FirewallRuleArgument()
    cls.FIREWALL_RULE_ARG.AddArgument(parser, operation_type='update')
    firewalls_utils.AddCommonArgs(
        parser,
        for_update=True,
        with_egress_support=cls.with_egress_firewall,
        with_service_account=cls.with_service_account)
    firewalls_utils.AddArgsForServiceAccount(parser, for_update=True)
    flags.AddEnableLogging(parser)
    flags.AddLoggingMetadata(parser, messages)


UpdateFirewall.detailed_help = {
    'brief':
        'Update a firewall rule.',
    'DESCRIPTION':
        """\
        *{command}* is used to update firewall rules that allow/deny
        incoming/outgoing traffic. The firewall rule will only be updated for
        arguments that are specifically passed. Other attributes will remain
        unaffected. The `action` flag (whether to allow or deny matching
        traffic) cannot be defined when updating a firewall rule; use
        `gcloud compute firewall-rules delete` to remove the rule instead.
        """,
    'EXAMPLES': """\
        To update the firewall rule ``RULE'' to enable logging, run:

          $ {command} RULE --enable-logging
        """,
}
