# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Shared flags for Cloud Workflows commands."""


from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.command_lib.util.args import labels_util
from googlecloudsdk.command_lib.util.args import map_util
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.core import properties
from googlecloudsdk.core.util import files
import six

_KEY_NAME_PATTERN = (
    r'^projects/[^/]+/locations/[^/]+/keyRings/[a-zA-Z0-9_-]+'
    '/cryptoKeys/[a-zA-Z0-9_-]+$'
)
_KEY_NAME_ERROR = (
    'KMS key name should match projects/{project}/locations/{location}'
    '/keyRings/{keyring}/cryptoKeys/{cryptokey} and only contain characters '
    'from the valid character set for a KMS key.'
)
USER_ENV_VARS_LIMIT = 20
CLEAR_ENVIRONMENT = object()


def LocationAttributeConfig():
  """Builds an AttributeConfig for the location resource."""
  return concepts.ResourceParameterAttributeConfig(
      name='location',
      fallthroughs=[
          deps.PropertyFallthrough(properties.FromString('workflows/location'))
      ],
      help_text=(
          'Cloud location for the {resource}. '
          ' Alternatively, set the property [workflows/location].'
      ),
  )


def WorkflowAttributeConfig():
  """Builds an AttributeConfig for the workflow resource."""
  return concepts.ResourceParameterAttributeConfig(
      name='workflow', help_text='Workflow for the {resource}.'
  )


def ExecutionAttributeConfig():
  """Builds an AttributeConfig for the execution resource."""
  return concepts.ResourceParameterAttributeConfig(
      name='execution', help_text='Execution for the {resource}.'
  )


def GetWorkflowResourceSpec():
  """Builds a ResourceSpec for the workflow resource."""
  return concepts.ResourceSpec(
      'workflows.projects.locations.workflows',
      resource_name='workflow',
      projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      locationsId=LocationAttributeConfig(),
      workflowsId=WorkflowAttributeConfig(),
  )


def GetExecutionResourceSpec():
  """Builds a ResourceSpec for the execution resource."""
  return concepts.ResourceSpec(
      'workflowexecutions.projects.locations.workflows.executions',
      resource_name='execution',
      projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      workflowsId=WorkflowAttributeConfig(),
      locationsId=LocationAttributeConfig(),
      executionsId=ExecutionAttributeConfig(),
  )


def AddWorkflowResourceArg(parser, verb):
  """Add a resource argument for a Cloud Workflows workflow.

  Args:
    parser: the parser for the command.
    verb: str, the verb to describe the resource, such as 'to update'.
  """
  concept_parsers.ConceptParser.ForResource(
      'workflow',
      GetWorkflowResourceSpec(),
      'Name of the workflow {}.'.format(verb),
      required=True,
  ).AddToParser(parser)


def AddExecutionResourceArg(parser, verb):
  """Add a resource argument for a Cloud Workflows execution.

  Args:
    parser: the parser for the command.
    verb: str, the verb to describe the resource, such as 'to update'.
  """
  concept_parsers.ConceptParser.ForResource(
      'execution',
      GetExecutionResourceSpec(),
      'Name of the execution {}.'.format(verb),
      required=True,
  ).AddToParser(parser)


def AddSourceArg(parser):
  """Adds argument for specifying source for the workflow."""
  parser.add_argument(
      '--source',
      help=(
          'Location of a workflow source code to deploy. Required on first '
          'deployment. Location needs to be defined as a path to a local file '
          'with the source code.'
      ),
  )


def AddDescriptionArg(parser):
  """Adds argument for specifying description of the workflow."""
  parser.add_argument(
      '--description', help='The description of the workflow to deploy.'
  )


def AddServiceAccountArg(parser):
  """Adds argument for specifying service account used by the workflow."""
  parser.add_argument(
      '--service-account',
      help=(
          'The service account that should be used as the workflow identity.'
          ' "projects/PROJECT_ID/serviceAccounts/" prefix may be skipped from'
          ' the full resource name, in that case "projects/-/serviceAccounts/"'
          ' is prepended to the service account ID.'
      ),
  )


def AddDataArg(parser):
  """Adds argument for specifying the data that will be passed to the workflow."""
  parser.add_argument(
      '--data',
      help=(
          'JSON string with data that will be passed to the workflow '
          'as an argument.'
      ),
  )


def AddLoggingArg(parser):
  """Adds argument for specifying the logging level for an execution."""
  log_level = base.ChoiceArgument(
      '--call-log-level',
      choices={
          'none': 'No logging level specified.',
          'log-all-calls': (
              'Log all calls to subworkflows or library functions and their'
              ' results.'
          ),
          'log-errors-only': 'Log when a call is stopped due to an exception.',
          'log-none': 'Perform no call logging.',
      },
      help_str='Level of call logging to apply during execution.',
      default='none',
  )
  log_level.AddToParser(parser)


def AddExecutionHistoryLevelArg(parser):
  """Adds argument for specifying the execution history level for an execution."""
  execution_history_level = base.ChoiceArgument(
      '--execution-history-level',
      choices={
          'none': 'No execution history level specified.',
          'execution-history-basic': (
              'Enable execution history basic feature.'
          ),
          'execution-history-detailed': (
              'Enable execution history detailed feature.'
          ),
      },
      help_str='Level of execution history to apply during execution.',
      default='none',
  )
  execution_history_level.AddToParser(parser)


def AddDisableOverflowBufferArg(parser):
  """Adds an argument for determining whether to backlog the execution."""
  parser.add_argument(
      '--disable-concurrency-quota-overflow-buffering',
      action='store_true',
      default=False,
      help=(
          'If set, the execution will not be backlogged when the concurrency '
          'quota is exhausted. Backlogged executions start when the '
          'concurrency quota becomes available.'
      ),
  )


def AddBetaLoggingArg(parser):
  """Adds argument for specifying the logging level for an execution."""
  log_level = base.ChoiceArgument(
      '--call-log-level',
      choices={
          'none': 'Perform no call logging.',
          'log-all-calls': (
              'Log all calls to subworkflows or library functions and their'
              ' results.'
          ),
          'log-errors-only': 'Log when a call is stopped due to an exception.',
      },
      help_str='Level of call logging to apply during execution.',
      default='none',
  )
  log_level.AddToParser(parser)


def AddWorkflowLoggingArg(parser):
  """Adds argument for specifying the logging level for a workflow."""
  log_level = base.ChoiceArgument(
      '--call-log-level',
      choices={
          'none': 'No logging level specified.',
          'log-all-calls': (
              'Log all calls to subworkflows or library functions and their'
              ' results.'
          ),
          'log-errors-only': 'Log when a call is stopped due to an exception.',
          'log-none': 'Perform no call logging.',
      },
      help_str='Level of call logging to apply by default for the workflow.',
      default='none',
  )
  log_level.AddToParser(parser)


def AddWorkflowTagsArg(parser):
  """Adds argument for specifying the tags for a workflow."""
  help_parts = [
      'List of tags KEY=VALUE pairs to bind.',
      'Each item must be expressed as',
      '"<tag-key-namespaced-name>=<tag-value-short-name>".\n',
      'Example: 123/environment=production,123/costCenter=marketing',
  ]
  tags = base.Argument(
      '--tags',
      metavar='KEY=VALUE',
      type=arg_parsers.ArgDict(),
      action=arg_parsers.UpdateAction,
      help='\n'.join(help_parts),
  )
  tags.AddToParser(parser)


def SetWorkflowsTagsArg(args, workflow, tags_message):
  """Sets --tags for the workflow based on the arguments."""
  if args.IsSpecified('tags'):
    tags = getattr(args, 'tags')
    if not tags:
      return None
    # Sorted for test stability
    workflow.tags = tags_message(additionalProperties=[
        tags_message.AdditionalProperty(key=key, value=value)
        for key, value in sorted(six.iteritems(tags))])


def AddWorkflowExecutionHistoryLevelArg(parser):
  """"Adds argument for specifying the execution history level for a workflow."""
  execution_history_level = base.ChoiceArgument(
      '--execution-history-level',
      choices={
          'none': 'No execution history level specified.',
          'execution-history-basic': (
              'Enable basic execution history.'
          ),
          'execution-history-detailed': (
              'Enable detailed execution history, including expected'
              ' iterations and in-scope variable values.'
          ),
      },
      help_str='Level of execution history to apply for the workflow.',
      default='none',
  )
  execution_history_level.AddToParser(parser)


def SetWorkflowLoggingArg(loglevel, workflow, updated_fields):
  """Sets --call-log-level for the workflow based on the arguments.

  Also updates updated_fields accordingly.

  Args:
    loglevel: Parsed callLogLevel to be set on the workflow.
    workflow: The workflow in which to set the call-log-level.
    updated_fields: A list to which the call-log-level field will be added if
      needed.
  """
  if loglevel is not None:
    workflow.callLogLevel = loglevel
    updated_fields.append('callLogLevel')


def SetWorkflowExecutionHistoryLevelArg(
    execution_history_level, workflow, updated_fields):
  """Sets --execution-history-level for the workflow based on the arguments.

  Also updates updated_fields accordingly.

  Args:
    execution_history_level: Parsed executionHistoryLevel to be set
    on the workflow.
    workflow: The workflow in which to set the execution-history-level.
    updated_fields: A list to which the execution-history-level field will
    be added if needed.
  """
  if execution_history_level is not None:
    workflow.executionHistoryLevel = execution_history_level
    updated_fields.append('executionHistoryLevel')


# Flags for CMEK
def AddKmsKeyFlags(parser):
  """Adds flags for configuring the CMEK key.

  Args:
    parser: The flag parser used for the specified command.
  """
  kmskey_group = parser.add_group(mutex=True, hidden=True)
  kmskey_group.add_argument(
      '--kms-key',
      type=arg_parsers.RegexpValidator(_KEY_NAME_PATTERN, _KEY_NAME_ERROR),
      help="""\
        Sets the user managed KMS crypto key used to encrypt the new Workflow
        Revision and the Executions associated with it.

        The KMS crypto key name should match the pattern
        `projects/${PROJECT}/locations/${LOCATION}/keyRings/${KEYRING}/cryptoKeys/${CRYPTOKEY}`
        where ${PROJECT} is the project, ${LOCATION} is the location of the key
        ring, and ${KEYRING} is the key ring that contains the ${CRYPTOKEY}
        crypto key.
      """,
  )
  kmskey_group.add_argument(
      '--clear-kms-key',
      action='store_true',
      help="""\
        Creates the new Workflow Revision and its associated Executions without
        the KMS key specified on the previous revision.
      """,
  )


def SetKmsKey(args, workflow, updated_fields):
  """Sets KMS key for the workflow based on the arguments.

  Also update updated_fields accordingly.

  Args:
    args: Args passed to the command.
    workflow: The workflow in which to set the KMS key.
    updated_fields: A list to which the KMS key field will be added if needed.
  """
  if args.IsSpecified('kms_key') or args.IsSpecified('clear_kms_key'):
    workflow.cryptoKeyName = None if args.clear_kms_key else args.kms_key
    updated_fields.append('cryptoKeyName')


def AddUserEnvVarsFlags(parser):
  """Adds flags for configuring user-defined environment variables."""
  userenvvars_group = parser.add_group(
      mutex=True,
      hidden=False,
      help="""\
        Flags to configure user-defined environment variables for a
        workflow.

        Keys can't be empty strings and can't start with `GOOGLE`
        or `WORKFLOWS`. We recommend that environment variable keys consist
        solely of uppercase letters, digits, and underscores (`_`), and that
        they don't begin with a digit. Consider prefixing your user-defined
        environment variables with a unique key to avoid conflicts with other
        variables.

        If your value contains commas, prefix the mapping with a different
        delimiter character enclosed between `^` (example 1). Use special
        characters in your shell with caution as they might not work as
        intended or need escaping (example 2 escapes a `$` in Bash).

        Example 1: --set-env-vars ^@^KEY1=ONE,VALUE,WITH,COMMAS@KEY2=VALUE2

        Example 2: --set-env-vars ^$^KEY1=VALUE1\\$KEY2=VALUE,WITH,COMMAS,TOO

        A maximum of 20 user-defined environment variables can be defined.
        Each definition string (`KEY=value`) is limited to 4 KiB.
        All keys and values are converted to strings.
      """,
  )

  userenvvars_group.add_argument(
      '--set-env-vars',
      type=arg_parsers.ArgDict(
          key_type=str,
          value_type=str,
          max_length=USER_ENV_VARS_LIMIT,
      ),
      action=arg_parsers.UpdateAction,
      metavar='KEY=VALUE',
      help="""\
        Sets environment variables for the workflow based on a comma-separated
        list of key-value pairs. Will overwrite a workflow's existing
        environment variables.

        Example:
        gcloud workflows deploy ${workflow_name} --set-env-vars policy=global,retry_count=5
      """,
  )

  userenvvars_group.add_argument(
      '--env-vars-file',
      metavar='FILE_PATH',
      type=map_util.ArgDictFile(key_type=str, value_type=str),
      help="""\
        Sets environment variables for the workflow to those stored in a local
        YAML file at the given path. All existing environment variables are
        removed before the new environment variables are added.

        Example:
        gcloud workflows deploy ${workflow_name} --env-vars-file=/path/to/env_vars.yaml

        Inside env_vars.yaml:\n
        policy: global\n
        retry_count: 5 # Service converts this to string "5"
      """,
  )

  userenvvars_group.add_argument(
      '--clear-env-vars',
      action='store_true',
      help="""\
        Clears all user-defined environment variables previously set for the
        workflow.

        Example:
        gcloud workflows deploy ${workflow_name} --clear-env-vars
      """,
  )

  userenvvars_group.add_argument(
      '--remove-env-vars',
      metavar='KEY',
      action=arg_parsers.UpdateAction,
      type=arg_parsers.ArgList(element_type=str),
      help="""\
        Removes user-defined environment variables from a workflow based
        on a list of environment variable keys to be removed.

        Example:
        gcloud workflows deploy ${workflow_name} --remove-env-vars policy,retry_count...
      """,
  )

  userenvvars_group.add_argument(
      '--update-env-vars',
      type=arg_parsers.ArgDict(key_type=str, value_type=str),
      action=arg_parsers.UpdateAction,
      metavar='KEY=VALUE',
      help="""\
        Updates existing or adds new user-defined environment variables based
        on a comma-separated list of key-value pairs.

        Example:
        gcloud workflows deploy ${workflow_name} --update-env-vars policy=regional,retry_count=2
      """,
  )


def ParseExecution(args):
  """Get and validate execution from the args."""
  return args.CONCEPTS.execution.Parse()


def ParseExecutionLabels(args):
  """Get and validate execution labels from the args."""
  messages = apis.GetClientInstance('workflowexecutions', 'v1').MESSAGES_MODULE
  return labels_util.ParseCreateArgs(args, messages.Execution.LabelsValue)


def ParseWorkflow(args):
  """Get and validate workflow from the args."""
  return args.CONCEPTS.workflow.Parse()


def SetSource(args, workflow, updated_fields):
  """Set source for the workflow based on the arguments.

  Also update updated_fields accordingly.
  Currently only local source file is supported.

  Args:
    args: Args passed to the command.
    workflow: The workflow in which to set the source configuration.
    updated_fields: A list to which an appropriate source field will be added.
  """
  if args.source:
    try:
      workflow.sourceContents = files.ReadFileContents(args.source)
    except files.MissingFileError:
      raise exceptions.BadArgumentException(
          '--source', 'specified file does not exist.'
      )
    updated_fields.append('sourceContents')


def SetDescription(args, workflow, updated_fields):
  """Set description for the workflow based on the arguments.

  Also update updated_fields accordingly.

  Args:
    args: Args passed to the command.
    workflow: The workflow in which to set the description.
    updated_fields: A list to which a description field will be added if needed.
  """
  if args.description is not None:
    workflow.description = args.description
    updated_fields.append('description')


def SetServiceAccount(args, workflow, updated_fields):
  """Set service account for the workflow based on the arguments.

  Also update updated_fields accordingly.

  Args:
    args: Args passed to the command.
    workflow: The workflow in which to set the service account.
    updated_fields: A list to which a service_account field will be added if
      needed.
  """
  if args.service_account is not None:
    prefix = ''
    if not args.service_account.startswith('projects/'):
      prefix = 'projects/-/serviceAccounts/'
    workflow.serviceAccount = prefix + args.service_account
    updated_fields.append('serviceAccount')


def SetLabels(labels, workflow, updated_fields):
  """Set labels for the workflow based on the arguments.

  Also update updated_fields accordingly.

  Args:
    labels: Labels parsed as string to be set on the workflow, or None in case
      the field shouldn't be set.
    workflow: The workflow in which to set the labels.
    updated_fields: A list to which a labels field will be added if needed.
  """
  if labels is not None:
    workflow.labels = labels
    updated_fields.append('labels')


def SetUserEnvVars(env_vars, workflow, updated_fields):
  """Sets user-defined environment variables.

  Also updates updated_fields accordingly.

  Args:
    env_vars: Parsed environment variables to be set on the workflow.
    workflow: The workflow in which to set the User Envrionment Variables.
    updated_fields: A list to which the userEnvVars field will be added if
      needed.
  """
  if env_vars is None:
    return
  workflow.userEnvVars = None if env_vars is CLEAR_ENVIRONMENT else env_vars
  updated_fields.append('userEnvVars')


def UpdateUserEnvVars(env_vars, workflow, updated_fields):
  """Updates user-defined environment variables.

  Also updates updated_fields accordingly.

  Args:
    env_vars: Parsed environment variables to be set on the workflow.
    workflow: The workflow in which to set the User Envrionment Variables.
    updated_fields: A list to which the userEnvVars field will be added if
      needed.
  """
  if env_vars is None:
    return
  env_vars_cls = apis.GetClientInstance(
      'workflows',
      'v1',
  ).MESSAGES_MODULE.Workflow.UserEnvVarsValue
  workflow.userEnvVars = env_vars_cls(
      additionalProperties=[
          env_vars_cls.AdditionalProperty(key=key, value=value)
          for key, value in sorted(env_vars.items())
      ]
  )
  updated_fields.append('userEnvVars')
