# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Utilities for parsing the cloud deploy resource to yaml definition."""

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

import collections
import datetime
import re

from dateutil import parser
from googlecloudsdk.calliope import exceptions as c_exceptions
from googlecloudsdk.command_lib.deploy import automation_util
from googlecloudsdk.command_lib.deploy import deploy_util
from googlecloudsdk.command_lib.deploy import exceptions
from googlecloudsdk.command_lib.deploy import target_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
from googlecloudsdk.core.resource import resource_property

PIPELINE_UPDATE_MASK = '*,labels'
DELIVERY_PIPELINE_KIND_V1BETA1 = 'DeliveryPipeline'
DELIVERY_PIPELINE = 'deliveryPipeline'
TARGET_KIND_V1BETA1 = 'Target'
TARGET = 'target'
AUTOMATION_KIND = 'Automation'
CUSTOM_TARGET_TYPE_KIND = 'CustomTargetType'
DEPLOY_POLICY_KIND = 'DeployPolicy'
API_VERSION_V1BETA1 = 'deploy.cloud.google.com/v1beta1'
API_VERSION_V1 = 'deploy.cloud.google.com/v1'
USAGE_CHOICES = ['RENDER', 'DEPLOY']
ACTION_CHOICES = [
    'ADVANCE',
    'APPROVE',
    'CANCEL',
    'CREATE',
    'IGNORE_JOB',
    'RETRY_JOB',
    'ROLLBACK',
    'TERMINATE_JOBRUN',
]
DAY_OF_WEEK_CHOICES = [
    'MONDAY',
    'TUESDAY',
    'WEDNESDAY',
    'THURSDAY',
    'FRIDAY',
    'SATURDAY',
    'SUNDAY',
]
INVOKER_CHOICES = ['USER', 'DEPLOY_AUTOMATION']
# If changing these fields also change them in the UI code.
NAME_FIELD = 'name'
ADVANCE_ROLLOUT_FIELD = 'advanceRollout'
PROMOTE_RELEASE_FIELD = 'promoteRelease'
REPAIR_ROLLOUT_FIELD = 'repairRollout'
WAIT_FIELD = 'wait'
LABELS_FIELD = 'labels'
ANNOTATIONS_FIELD = 'annotations'
SELECTOR_FIELD = 'selector'
RULES_FIELD = 'rules'
TARGET_ID_FIELD = 'targetId'
ID_FIELD = 'id'
ADVANCE_ROLLOUT_RULE_FIELD = 'advanceRolloutRule'
PROMOTE_RELEASE_RULE_FIELD = 'promoteReleaseRule'
REPAIR_ROLLOUT_RULE_FIELD = 'repairRolloutRule'
ROLLOUT_RESTRICTIONS_FIELD = 'rolloutRestriction'
TIMED_PROMOTE_RELEASE_RULE_FIELD = 'timedPromoteReleaseRule'
DESTINATION_TARGET_ID_FIELD = 'destinationTargetId'
SOURCE_PHASES_FIELD = 'sourcePhases'
PHASES_FIELD = 'phases'
DESTINATION_PHASE_FIELD = 'destinationPhase'
DISABLE_ROLLBACK_IF_ROLLOUT_PENDING = 'disableRollbackIfRolloutPending'
TARGET_FIELD = 'target'
METADATA_FIELDS = [ANNOTATIONS_FIELD, LABELS_FIELD]
EXCLUDE_FIELDS = [
    'createTime',
    'etag',
    'uid',
    'updateTime',
    NAME_FIELD,
] + METADATA_FIELDS
JOBS_FIELD = 'jobs'
RETRY_FIELD = 'retry'
ATTEMPTS_FIELD = 'attempts'
ROLLBACK_FIELD = 'rollback'
REPAIR_PHASES_FIELD = 'repairPhases'
BACKOFF_MODE_FIELD = 'backoffMode'
BACKOFF_CHOICES = ['BACKOFF_MODE_LINEAR', 'BACKOFF_MODE_EXPONENTIAL']
BACKOFF_CHOICES_SHORT = ['LINEAR', 'EXPONENTIAL']
TARGETS_FIELD = 'targets'
SCHEDULE_FIELD = 'schedule'
TIME_ZONE_FIELD = 'timeZone'
STRATEGY_FIELD = 'strategy'
STANDARD_FIELD = 'standard'
CANARY_FIELD = 'canary'
CANARY_DEPLOYMENT_FIELD = 'canaryDeployment'
CUSTOM_CANARY_DEPLOYMENT_FIELD = 'customCanaryDeployment'
PHASE_CONFIGS_FIELD = 'phaseConfigs'
VERIFY_CONFIG_FIELD = 'verifyConfig'
PREDEPLOY_FIELD = 'predeploy'
POSTDEPLOY_FIELD = 'postdeploy'
ANALYSIS_FIELD = 'analysis'
CUSTOM_CHECKS_FIELD = 'customChecks'
GOOGLE_CLOUD_FIELD = 'googleCloud'
ALERT_POLICY_CHECKS_FIELD = 'alertPolicyChecks'
TASKS_FIELD = 'tasks'
CONFIG_TASK_FIELD = 'config'
CONTAINERS_TASK_FIELD = 'containersTask'
CONTAINERS_FIELD = 'containers'
ENV_FIELD = 'env'
LABELS_FIELD = 'labels'
DEPLOY_FIELD = 'deploy'
RENDER_FIELD = 'render'


def ParseDeployConfig(messages, manifests, region):
  """Parses the declarative definition of the resources into message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    manifests: [str], the list of parsed resource yaml definitions.
    region: str, location ID.

  Returns:
    A dictionary of resource kind and message.
  Raises:
    exceptions.CloudDeployConfigError, if the declarative definition is
    incorrect.
  """
  resource_dict = {
      DELIVERY_PIPELINE_KIND_V1BETA1: [],
      TARGET_KIND_V1BETA1: [],
      AUTOMATION_KIND: [],
      CUSTOM_TARGET_TYPE_KIND: [],
      DEPLOY_POLICY_KIND: [],
  }
  project = properties.VALUES.core.project.GetOrFail()
  _ValidateConfig(manifests)
  for manifest in manifests:
    _ParseV1Config(
        messages, manifest['kind'], manifest, project, region, resource_dict
    )

  return resource_dict


def _ValidateConfig(manifests):
  """Validates the manifests.

  Args:
     manifests: [str], the list of parsed resource yaml definitions.

  Raises:
    exceptions.CloudDeployConfigError, if there are errors in the manifests
    (e.g. required field is missing, duplicate resource names).
  """
  resource_type_to_names = collections.defaultdict(list)
  for manifest in manifests:
    api_version = manifest.get('apiVersion')
    if not api_version:
      raise exceptions.CloudDeployConfigError(
          'missing required field .apiVersion'
      )
    resource_type = manifest.get('kind')
    if resource_type is None:
      raise exceptions.CloudDeployConfigError('missing required field .kind')
    api_version = manifest['apiVersion']
    if api_version not in {API_VERSION_V1BETA1, API_VERSION_V1}:
      raise exceptions.CloudDeployConfigError(
          'api version {} not supported'.format(api_version)
      )
    metadata = manifest.get('metadata')
    if not metadata or not metadata.get(NAME_FIELD):
      raise exceptions.CloudDeployConfigError(
          'missing required field .metadata.name in {}'.format(
              manifest.get('kind')
          )
      )
    # Populate a dictionary with resource_type: [names].
    # E.g. [TARGET: "target1, target2"]
    resource_type_to_names[resource_type].append(metadata.get(NAME_FIELD))
  _CheckDuplicateResourceNames(resource_type_to_names)


def _CheckDuplicateResourceNames(resource_type_to_names):
  """Checks if there are any duplicate resource names per resource type.

  Args:
     resource_type_to_names: dict[str,[str]], dict of resource type and names.

  Raises:
    exceptions.CloudDeployConfigError, if there are duplicate names for a given
    resource type.
  """
  errors = []
  for k, names in resource_type_to_names.items():
    dups = set()
    # For a given resource type, iterate through the resource names and check
    # for duplicates.
    for name in names:
      if names.count(name) > 1:
        dups.add(name)
    if dups:
      errors.append('{} has duplicate name(s): {}'.format(k, dups))
  if errors:
    raise exceptions.CloudDeployConfigError(errors)


def _ParseV1Config(messages, kind, manifest, project, region, resource_dict):
  """Parses the Cloud Deploy v1 and v1beta1 resource specifications into message.

       This specification version is KRM complied and should be used after
       private review.

  Args:
     messages: module containing the definitions of messages for Cloud Deploy.
     kind: str, name of the resource schema.
     manifest: dict[str,str], cloud deploy resource yaml definition.
     project: str, gcp project.
     region: str, ID of the location.
     resource_dict: dict[str,optional[message]], a dictionary of resource kind
       and message.

  Raises:
    exceptions.CloudDeployConfigError, if the declarative definition is
    incorrect.
  """
  metadata = manifest.get('metadata')
  if kind == DELIVERY_PIPELINE_KIND_V1BETA1:
    resource_type = deploy_util.ResourceType.DELIVERY_PIPELINE
    resource, resource_ref = _CreateDeliveryPipelineResource(
        messages, metadata[NAME_FIELD], project, region
    )
  elif kind == TARGET_KIND_V1BETA1:
    resource_type = deploy_util.ResourceType.TARGET
    resource, resource_ref = _CreateTargetResource(
        messages, metadata[NAME_FIELD], project, region
    )
  elif kind == AUTOMATION_KIND:
    resource_type = deploy_util.ResourceType.AUTOMATION
    resource, resource_ref = _CreateAutomationResource(
        messages, metadata[NAME_FIELD], project, region
    )
  elif kind == CUSTOM_TARGET_TYPE_KIND:
    resource_type = deploy_util.ResourceType.CUSTOM_TARGET_TYPE
    resource, resource_ref = _CreateCustomTargetTypeResource(
        messages, metadata[NAME_FIELD], project, region
    )
  elif kind == DEPLOY_POLICY_KIND:
    resource_type = deploy_util.ResourceType.DEPLOY_POLICY
    resource, resource_ref = _CreateDeployPolicyResource(
        messages, metadata[NAME_FIELD], project, region
    )
  else:
    raise exceptions.CloudDeployConfigError(
        'kind {} not supported'.format(kind)
    )

  if '/' in resource_ref.Name():
    raise exceptions.CloudDeployConfigError(
        'resource ID "{}" contains /.'.format(resource_ref.Name())
    )

  for field in manifest:
    if field not in ['apiVersion', 'kind', 'metadata', 'deliveryPipeline']:
      value = manifest.get(field)
      if field == 'executionConfigs':
        SetExecutionConfig(messages, resource, resource_ref, value)
        continue
      if field == 'deployParameters' and kind == TARGET_KIND_V1BETA1:
        SetDeployParametersForTarget(messages, resource, resource_ref, value)
        continue
      if field == 'customTarget' and kind == TARGET_KIND_V1BETA1:
        SetCustomTarget(resource, value, project, region)
        continue
      if field == TASKS_FIELD and kind == CUSTOM_TARGET_TYPE_KIND:
        SetCustomTargetTasks(messages, resource, value)
        continue
      if field == 'associatedEntities' and kind == TARGET_KIND_V1BETA1:
        SetAssociatedEntities(messages, resource, resource_ref, value)
        continue
      if field == 'serialPipeline' and kind == DELIVERY_PIPELINE_KIND_V1BETA1:
        serial_pipeline = manifest.get('serialPipeline')
        _EnsureIsType(
            serial_pipeline,
            dict,
            'failed to parse pipeline {}, serialPipeline is defined incorrectly'
            .format(resource_ref.Name()),
        )
        stages = serial_pipeline.get('stages')
        _EnsureIsType(
            stages,
            list,
            'failed to parse pipeline {}, stages are defined incorrectly'
            .format(resource_ref.Name()),
        )
        for stage in stages:
          SetDeployParametersForPipelineStage(messages, resource_ref, stage)
          SetMapFieldsForPipelineStage(messages, stage)

      if field == SELECTOR_FIELD and kind == AUTOMATION_KIND:
        SetAutomationSelector(messages, resource, value)
        continue
      if field == RULES_FIELD and kind == AUTOMATION_KIND:
        SetAutomationRules(messages, resource, resource_ref, value)
        continue
      if field == RULES_FIELD and kind == DEPLOY_POLICY_KIND:
        rules = manifest.get('rules')
        _EnsureIsType(
            rules,
            list,
            'failed to parse deploy policy {}, rules are defined incorrectly'
            .format(resource_ref.Name()),
        )
        SetPolicyRules(messages, resource, rules)
        continue
      if field == 'selectors' and kind == DEPLOY_POLICY_KIND:
        selectors = manifest.get('selectors')
        _EnsureIsType(
            selectors,
            list,
            'failed to parse deploy policy {}, selectors are defined'
            ' incorrectly'.format(resource_ref.Name()),
        )
        SetDeployPolicySelector(messages, resource, selectors)
        continue
      setattr(resource, field, value)

  # Sets the properties in metadata.
  for field in metadata:
    if field not in [NAME_FIELD, ANNOTATIONS_FIELD, LABELS_FIELD]:
      setattr(resource, field, metadata.get(field))
  deploy_util.SetMetadata(
      messages,
      resource,
      resource_type,
      metadata.get(ANNOTATIONS_FIELD),
      metadata.get(LABELS_FIELD),
  )

  resource_dict[kind].append(resource)


def SetMapFieldsForPipelineStage(messages, stage):
  """Sets any map fields on a pipeline stage.

  This includes environment variables defined on a Task and labels defined on an
  Analysis Alert Policy Check.

  Args:
   messages: module containing the definitions of messages for Cloud Deploy.
   stage: Cloud Deploy Stage message.
  """
  stage_strategy = stage.get(STRATEGY_FIELD)
  if not stage_strategy:
    return

  if STANDARD_FIELD in stage_strategy:
    SetMapFieldsOnStrategy(messages, stage_strategy.get(STANDARD_FIELD))

  elif CANARY_FIELD in stage_strategy:
    # Canary deployment
    if stage_strategy.get(CANARY_FIELD).get(CANARY_DEPLOYMENT_FIELD):
      SetMapFieldsOnStrategy(
          messages,
          stage_strategy.get(CANARY_FIELD).get(CANARY_DEPLOYMENT_FIELD),
      )
    # Custom canary deployment
    else:
      for phase_config in (
          stage_strategy.get(CANARY_FIELD)
          .get(CUSTOM_CANARY_DEPLOYMENT_FIELD)
          .get(PHASE_CONFIGS_FIELD)
      ):
        SetMapFieldsOnStrategy(messages, phase_config)


def SetMapFieldsOnStrategy(messages, strategy):
  """Sets any map fields on a strategy or phase config.

  Args:
   messages: module containing the definitions of messages for Cloud Deploy.
   strategy: Cloud Deploy Strategy or PhaseConfig message.
  """
  if strategy.get(VERIFY_CONFIG_FIELD):
    for task in strategy.get(VERIFY_CONFIG_FIELD).get(TASKS_FIELD):
      SetEnvForTask(messages, task)

  if strategy.get(PREDEPLOY_FIELD) and strategy.get(PREDEPLOY_FIELD).get(
      TASKS_FIELD
  ):
    for task in strategy.get(PREDEPLOY_FIELD).get(TASKS_FIELD):
      SetEnvForTask(messages, task)

  if strategy.get(POSTDEPLOY_FIELD) and strategy.get(POSTDEPLOY_FIELD).get(
      TASKS_FIELD
  ):
    for task in strategy.get(POSTDEPLOY_FIELD).get(TASKS_FIELD):
      SetEnvForTask(messages, task)

  if strategy.get(ANALYSIS_FIELD):
    if strategy.get(ANALYSIS_FIELD).get(CUSTOM_CHECKS_FIELD):
      for custom_check in strategy.get(ANALYSIS_FIELD).get(CUSTOM_CHECKS_FIELD):
        SetEnvForTask(messages, custom_check.get('task'))
    if strategy.get(ANALYSIS_FIELD).get(GOOGLE_CLOUD_FIELD):
      for alert_policy_check in (
          strategy.get(ANALYSIS_FIELD)
          .get(GOOGLE_CLOUD_FIELD)
          .get(ALERT_POLICY_CHECKS_FIELD)
      ):
        SetLabelsForAlertPolicyCheck(messages, alert_policy_check)


def SetEnvForTask(messages, task):
  """Sets environment variables for a Task.

  Args:
   messages: module containing the definitions of messages for Cloud Deploy.
   task: Cloud Deploy Task message.
  """

  # For each task type (ConfigTask or ContainersTask), turn the provided string
  # map into an EnvValue message.
  if task.get(CONFIG_TASK_FIELD):
    config_task = task.get(CONFIG_TASK_FIELD)
    if not config_task.get(ENV_FIELD):
      return
    config_task_message = getattr(messages, 'ConfigTask')
    env_message = config_task_message.EnvValue
    env_dict = env_message()
    for key, value in config_task.get(ENV_FIELD).items():
      env_dict.additionalProperties.append(
          env_message.AdditionalProperty(key=key, value=value)
      )
    config_task[ENV_FIELD] = env_dict
  elif task.get(CONTAINERS_TASK_FIELD):
    containers_task = task.get(CONTAINERS_TASK_FIELD)
    for container in containers_task.get(CONTAINERS_FIELD):
      if not container.get(ENV_FIELD):
        continue
      container_message = getattr(messages, 'Container')
      env_message = container_message.EnvValue
      env_dict = env_message()
      for key, value in container.get(ENV_FIELD).items():
        env_dict.additionalProperties.append(
            env_message.AdditionalProperty(key=key, value=value)
        )
      container[ENV_FIELD] = env_dict


def SetLabelsForAlertPolicyCheck(messages, alert_policy_check):
  """Sets labels defined on an Analysis Google Cloud Alert Policy Check.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    alert_policy_check: Cloud Deploy Alert Policy Check message for an Analysis
    with Google Cloud.
  """
  if alert_policy_check is None:
    return
  labels_message = getattr(messages, 'AlertPolicyCheck').LabelsValue
  labels_dict = labels_message()
  for key, value in alert_policy_check.get(LABELS_FIELD).items():
    labels_dict.additionalProperties.append(
        labels_message.AdditionalProperty(
            key=resource_property.ConvertToSnakeCase(key), value=value
        )
    )
  alert_policy_check[LABELS_FIELD] = labels_dict


def SetPolicyRules(messages, policy, rules):
  """Sets the rules field of cloud deploy policy message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    policy:  googlecloudsdk.generated_clients.apis.clouddeploy.DeployPolicy
      message.
    rules: [googlecloudsdk.generated_clients.apis.clouddeploy.PolicyRule], list
      of PolicyRule messages.

  Raises:
    arg_parsers.ArgumentTypeError: if usage is not a valid enum.
  """
  # Go through each rule and parse the rolloutRestriction field.
  for pv_rule in rules:
    rollout_restriction_message = messages.RolloutRestriction()
    if pv_rule.get(ROLLOUT_RESTRICTIONS_FIELD):
      rollout_restriction = pv_rule.get(ROLLOUT_RESTRICTIONS_FIELD)
      _SetRolloutRestriction(
          messages, rollout_restriction_message, rollout_restriction, policy
      )
    else:
      policy.rules.append(pv_rule)


def _SetRolloutRestriction(
    messages, rollout_restriction_message, rollout_restriction, policy
):
  """Sets the rolloutRestriction field of cloud deploy policy message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    rollout_restriction_message:
      googlecloudsdk.generated_clients.apis.clouddeploy.RolloutRestriction
      message.
    rollout_restriction: value of the rolloutRestriction field in the manifest.
    policy:  googlecloudsdk.generated_clients.apis.clouddeploy.DeployPolicy
      message.

  Raises:
    arg_parsers.ArgumentTypeError: if usage is not a valid enum.
  """
  for field in rollout_restriction:
    # The value of actions and invokers are enums, so they need special
    # treatment. timeWindow also needs special treatment because it has an
    # enum within it.
    if field != 'actions' and field != 'invokers' and field != 'timeWindows':
      # If the field doesn't need special treatment (e.g. id) set it
      # on the rolloutRestriction message.
      setattr(
          rollout_restriction_message, field, rollout_restriction.get(field)
      )

  actions = rollout_restriction.get('actions', [])
  for action in actions:
    rollout_restriction_message.actions.append(
        # converts a string literal in rolloutRestriction.actions to an Enum.
        arg_utils.ChoiceToEnum(
            action,
            messages.RolloutRestriction.ActionsValueListEntryValuesEnum,
            valid_choices=ACTION_CHOICES,
        )
    )

  invokers = rollout_restriction.get('invokers', [])
  for invoker in invokers:
    rollout_restriction_message.invokers.append(
        # converts a string literal in rolloutRestriction.invokers to an Enum.
        arg_utils.ChoiceToEnum(
            invoker,
            messages.RolloutRestriction.InvokersValueListEntryValuesEnum,
            valid_choices=INVOKER_CHOICES,
        )
    )
  # Parse and set the timeWindow field on the rolloutRestriction message.
  time_windows = rollout_restriction.get('timeWindows')
  time_windows_message = messages.TimeWindows()
  _SetTimeWindows(messages, time_windows_message, time_windows)
  rollout_restriction_message.timeWindows = time_windows_message
  # Set the rolloutRestriction field on the policy rule message.
  policy_rule_message = messages.PolicyRule()
  policy_rule_message.rolloutRestriction = rollout_restriction_message
  policy.rules.append(policy_rule_message)


def _SetTimeWindows(messages, time_windows_message, time_windows):
  """Sets the timeWindow field of cloud deploy resource message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    time_windows_message:
      googlecloudsdk.generated_clients.apis.clouddeploy.TimeWindows message.
    time_windows: value of the timeWindows field.

  Raises:
    arg_parsers.ArgumentTypeError: if usage is not a valid enum.
  """
  setattr(time_windows_message, 'timeZone', time_windows.get('timeZone'))

  # Go through each oneTimeWindow and parse the fields.
  one_time_windows = time_windows.get('oneTimeWindows', [])
  for one_time_window in one_time_windows:
    _SetOneTimeWindow(messages, one_time_window, time_windows_message)

  # Go through each weeklyWindow and parse the fields.
  weekly_windows = time_windows.get('weeklyWindows', [])
  for weekly_window in weekly_windows:
    _SetWeeklyWindow(messages, weekly_window, time_windows_message)


def _SetOneTimeWindow(messages, one_time_window, time_windows_message):
  """Sets the oneTimeWindow field of a timeWindows message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    one_time_window: value of the oneTimeWindow field.
    time_windows_message:
      googlecloudsdk.generated_clients.apis.clouddeploy.TimeWindows message.

  Raises:
    c_exceptions.InvalidArgumentException: if the start or end field is not a
    valid ISO 8601 string.
  """
  one_time_window_message = messages.OneTimeWindow()
  for field in one_time_window:
    if field not in ['start', 'end']:
      raise c_exceptions.InvalidArgumentException(
          field,
          'invalid input in a one-time window: {}. Use "start" and "end" to'
          ' specify the date/times. For example: "start: 2024-12-24 17:00"'
          .format(one_time_window.get(field)),
      )
  # Parse the ISO 8601 string from YAML into the date and time fields in the
  # API.
  if one_time_window.get('start'):
    _SetDateTimeFields(
        one_time_window, one_time_window_message, messages, 'start'
    )
  if one_time_window.get('end'):
    _SetDateTimeFields(
        one_time_window, one_time_window_message, messages, 'end'
    )
  time_windows_message.oneTimeWindows.append(one_time_window_message)


def _SetDateTimeFields(
    one_time_window, one_time_window_message, messages, field_name
):
  """Sets the start/end date time fields on the oneTimeWindow message.

  Args:
    one_time_window: value of the oneTimeWindow field.
    one_time_window_message:
      googlecloudsdk.generated_clients.apis.clouddeploy.OneTimeWindow message.
    messages: module containing the definitions of messages for Cloud Deploy.
    field_name: the field to set (start or end).

  Raises:
    c_exceptions.InvalidArgumentException: if the start or end field is not a
    valid ISO 8601 string or contains a timezone offset or UTC.
  """
  date_str = one_time_window.get(field_name)
  try:
    date_time = parser.isoparse(date_str)
  except ValueError:
    raise c_exceptions.InvalidArgumentException(
        field_name,
        'invalid date string: "{}". Must be a valid date in ISO 8601 format'
        ' (e.g. {}: 2024-12-24 17:00)'.format(date_str, field_name),
    )
  except TypeError:
    raise c_exceptions.InvalidArgumentException(
        field_name,
        'invalid date string: {}. Make sure to put quotes around the date'
        " string (e.g. {}: '2024-09-27 18:30:31.123') so that it is interpreted"
        ' as a string and not a yaml timestamp.'.format(date_str, field_name),
    )
  if date_time.tzinfo is not None:
    raise c_exceptions.InvalidArgumentException(
        field_name,
        'invalid date string: {}. Do not include a timezone or timezone offset'
        ' in the date string. Specify the timezone in the timeZone field.'
        .format(date_str),
    )
  # Set the date field (e.g. startDate or endDate).
  date_obj = _ConvertDate(date_time, messages)
  date_field = '{}Date'.format(field_name)
  setattr(one_time_window_message, date_field, date_obj)
  # Set the time field (e.g. startTime or endTime)
  time_obj = _ConvertTime(date_time, messages)
  time_field = '{}Time'.format(field_name)
  setattr(one_time_window_message, time_field, time_obj)


def _ConvertDate(date_time_obj, messages):
  """Converts a dateTime object to a Date message."""
  return messages.Date(
      year=date_time_obj.year, month=date_time_obj.month, day=date_time_obj.day
  )


def _ConvertTime(date_time_obj, messages):
  """Converts a dateTime object to a TimeOfDay message."""
  return messages.TimeOfDay(
      hours=date_time_obj.hour,
      minutes=date_time_obj.minute,
      seconds=date_time_obj.second,
      nanos=date_time_obj.microsecond * 1000,
  )


def _SetWeeklyWindow(messages, weekly_window, time_windows_message):
  """Sets the weeklyWindow field of a timeWindows message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    weekly_window: value of the weeklyWindow field.
    time_windows_message:
      googlecloudsdk.generated_clients.apis.clouddeploy.TimeWindows message.

  Raises:
    arg_parsers.ArgumentTypeError: if day of week is not a valid enum.
    c_exceptions.InvalidArgumentException: if the startTime or endTime field is
    not a
    valid ISO 8601 string.
  """
  weekly_window_message = messages.WeeklyWindow()
  for field in weekly_window:

    if field == 'startTime' or field == 'endTime':
      # The startTime and endTime fields need to be set with a string.
      if not isinstance(weekly_window.get(field), str):
        raise c_exceptions.InvalidArgumentException(
            field,
            'invalid input in a weekly window: {}. Use "startTime" and'
            ' "endTime" to specify the times. For example: "startTime: 17:00"'
            .format(weekly_window.get(field)),
        )
      _SetTime(messages, weekly_window_message, weekly_window.get(field), field)
  days_of_week = weekly_window.get('daysOfWeek') or []
  for d in days_of_week:
    weekly_window_message.daysOfWeek.append(
        # converts a string literal in weeklyWindow.daysOfWeek to an Enum.
        arg_utils.ChoiceToEnum(
            d,
            messages.WeeklyWindow.DaysOfWeekValueListEntryValuesEnum,
            valid_choices=DAY_OF_WEEK_CHOICES,
        )
    )
  time_windows_message.weeklyWindows.append(weekly_window_message)


def _SetTime(messages, weekly_window_message, value, field):
  """Sets the time field of a weeklyWindow message."""
  if isinstance(value, str):
    # First, check if the hour is 24. If so replace with 00, because
    # fromisoformat() doesn't support 24.
    hour_24 = False
    if value.startswith('24:'):
      hour_24 = True
      value = value.replace('24', '00', 1)
    try:
      time_obj = datetime.time.fromisoformat(value)
    except ValueError:
      raise c_exceptions.InvalidArgumentException(
          field,
          'invalid time string: "{}" for field {}'.format(value, field),
      )
    hour_value = time_obj.hour
    if hour_24:
      hour_value = 24
    time_of_day = messages.TimeOfDay(
        hours=hour_value,
        minutes=time_obj.minute,
        seconds=time_obj.second,
        nanos=time_obj.microsecond * 1000,
    )
    setattr(weekly_window_message, field, time_of_day)
  else:
    setattr(weekly_window_message, field, value)


def _CreateTargetResource(messages, target_name_or_id, project, region):
  """Creates target resource with full target name and the resource reference."""
  resource = messages.Target()
  resource_ref = target_util.TargetReference(target_name_or_id, project, region)
  resource.name = resource_ref.RelativeName()

  return resource, resource_ref


def _CreateDeliveryPipelineResource(
    messages, delivery_pipeline_name, project, region
):
  """Creates delivery pipeline resource with full delivery pipeline name and the resource reference."""
  resource = messages.DeliveryPipeline()
  resource_ref = resources.REGISTRY.Parse(
      delivery_pipeline_name,
      collection='clouddeploy.projects.locations.deliveryPipelines',
      params={
          'projectsId': project,
          'locationsId': region,
          'deliveryPipelinesId': delivery_pipeline_name,
      },
  )
  resource.name = resource_ref.RelativeName()

  return resource, resource_ref


def _CreateAutomationResource(messages, name, project, region):
  resource = messages.Automation()
  resource_ref = automation_util.AutomationReference(name, project, region)
  resource.name = resource_ref.RelativeName()

  return resource, resource_ref


def _CreateCustomTargetTypeResource(messages, name, project, region):
  """Creates custom target type resource with full name and the resource reference."""
  resource = messages.CustomTargetType()
  resource_ref = resources.REGISTRY.Parse(
      name,
      collection='clouddeploy.projects.locations.customTargetTypes',
      params={
          'projectsId': project,
          'locationsId': region,
          'customTargetTypesId': name,
      },
  )
  resource.name = resource_ref.RelativeName()

  return resource, resource_ref


def _CreateDeployPolicyResource(messages, name, project, region):
  """Creates deploy policy resource with full name and the resource reference."""
  resource = messages.DeployPolicy()
  resource_ref = resources.REGISTRY.Parse(
      name,
      collection='clouddeploy.projects.locations.deployPolicies',
      params={
          'projectsId': project,
          'locationsId': region,
          'deployPoliciesId': name,
      },
  )
  resource.name = resource_ref.RelativeName()

  return resource, resource_ref


def ProtoToManifest(resource, resource_ref, kind):
  """Converts a resource message to a cloud deploy resource manifest.

  The manifest can be applied by 'deploy apply' command.

  Args:
    resource: message in googlecloudsdk.generated_clients.apis.clouddeploy.
    resource_ref: cloud deploy resource object.
    kind: kind of the cloud deploy resource

  Returns:
    A dictionary that represents the cloud deploy resource.
  """
  manifest = collections.OrderedDict(
      apiVersion=API_VERSION_V1, kind=kind, metadata={}
  )

  for k in METADATA_FIELDS:
    v = getattr(resource, k)
    # Skips the 'zero' values in the message.
    if v:
      manifest['metadata'][k] = v
  # Sets the name to resource ID instead of the full name.
  if kind == AUTOMATION_KIND:
    manifest['metadata'][NAME_FIELD] = (
        resource_ref.AsDict()['deliveryPipelinesId'] + '/' + resource_ref.Name()
    )
  else:
    manifest['metadata'][NAME_FIELD] = resource_ref.Name()

  for f in resource.all_fields():
    if f.name in EXCLUDE_FIELDS:
      continue
    v = getattr(resource, f.name)
    # Skips the 'zero' values in the message.
    if v:
      if f.name == SELECTOR_FIELD and kind == AUTOMATION_KIND:
        ExportAutomationSelector(manifest, v)
        continue
      if f.name == RULES_FIELD and kind == AUTOMATION_KIND:
        ExportAutomationRules(manifest, v)
        continue
      # Special handling for the deploy policy rules field.
      # The rollout restriction rule accepts date/time strings in YAML which
      # then need to be parsed into the correct fields in the API.
      if f.name == RULES_FIELD and kind == DEPLOY_POLICY_KIND:
        ExportDeployPolicyRules(manifest, v)
        continue
      manifest[f.name] = v

  return manifest


def SetExecutionConfig(messages, target, target_ref, execution_configs):
  """Sets the executionConfigs field of cloud deploy resource message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    target:  googlecloudsdk.generated_clients.apis.clouddeploy.Target message.
    target_ref: protorpc.messages.Message, target resource object.
    execution_configs:
      [googlecloudsdk.generated_clients.apis.clouddeploy.ExecutionConfig], list
      of ExecutionConfig messages.

  Raises:
    arg_parsers.ArgumentTypeError: if usage is not a valid enum.
  """
  _EnsureIsType(
      execution_configs,
      list,
      'failed to parse target {}, executionConfigs are defined incorrectly'
      .format(target_ref.Name()),
  )
  for config in execution_configs:
    execution_config_message = messages.ExecutionConfig()
    for field in config:
      # the value of usages field has enum, which needs special treatment.
      if field != 'usages':
        setattr(execution_config_message, field, config.get(field))
    usages = config.get('usages') or []
    for usage in usages:
      execution_config_message.usages.append(
          # converts a string literal in executionConfig.usages to an Enum.
          arg_utils.ChoiceToEnum(
              usage,
              messages.ExecutionConfig.UsagesValueListEntryValuesEnum,
              valid_choices=USAGE_CHOICES,
          )
      )

    target.executionConfigs.append(execution_config_message)


def SetDeployParametersForPipelineStage(messages, pipeline_ref, stage):
  """Sets the deployParameter field of cloud deploy delivery pipeline stage message.

  Args:
   messages: module containing the definitions of messages for Cloud Deploy.
   pipeline_ref: protorpc.messages.Message, delivery pipeline resource object.
   stage: dict[str,str], cloud deploy stage yaml definition.
  """

  deploy_parameters = stage.get('deployParameters')
  if deploy_parameters is None:
    return

  _EnsureIsType(
      deploy_parameters,
      list,
      'failed to parse stages of pipeline {}, deployParameters are defined'
      ' incorrectly'.format(pipeline_ref.Name()),
  )
  dps_message = getattr(messages, 'DeployParameters')
  dps_values = []

  for dp in deploy_parameters:
    dps_value = dps_message()
    values = dp.get('values')
    if values:
      values_message = dps_message.ValuesValue
      values_dict = values_message()
      _EnsureIsType(
          values,
          dict,
          'failed to parse stages of pipeline {}, deployParameter values are'
          'defined incorrectly'.format(pipeline_ref.Name()),
      )
      for key, value in values.items():
        values_dict.additionalProperties.append(
            values_message.AdditionalProperty(key=key, value=value)
        )
      dps_value.values = values_dict

    match_target_labels = dp.get('matchTargetLabels')
    if match_target_labels:
      mtls_message = dps_message.MatchTargetLabelsValue
      mtls_dict = mtls_message()

      for key, value in match_target_labels.items():
        mtls_dict.additionalProperties.append(
            mtls_message.AdditionalProperty(key=key, value=value)
        )
      dps_value.matchTargetLabels = mtls_dict

    dps_values.append(dps_value)

  stage['deployParameters'] = dps_values


def SetDeployParametersForTarget(
    messages, target, target_ref, deploy_parameters
):
  """Sets the deployParameters field of cloud deploy target message.

  Args:
   messages: module containing the definitions of messages for Cloud Deploy.
   target: googlecloudsdk.generated_clients.apis.clouddeploy.Target message.
   target_ref: protorpc.messages.Message, target resource object.
   deploy_parameters: dict[str,str], a dict of deploy parameters (key,value)
     pairs.
  """

  _EnsureIsType(
      deploy_parameters,
      dict,
      'failed to parse target {}, deployParameters are defined incorrectly'
      .format(target_ref.Name()),
  )

  dps_message = getattr(
      messages, deploy_util.ResourceType.TARGET.value
  ).DeployParametersValue
  dps_value = dps_message()
  for key, value in deploy_parameters.items():
    dps_value.additionalProperties.append(
        dps_message.AdditionalProperty(key=key, value=value)
    )
  target.deployParameters = dps_value


def SetAssociatedEntities(messages, target, target_ref, associated_entities):
  """Sets the associatedEntities field in the cloud deploy target message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    target: googlecloudsdk.generated_clients.apis.clouddeploy.Target message.
    target_ref: protorpc.messages.Message, target resource object.
    associated_entities: dict[str, associated_entities], a dict of associated
      entities (key,value) pairs.
  """

  _EnsureIsType(
      associated_entities,
      dict,
      'failed to parse target {}, associatedEntities are defined incorrectly'
      .format(target_ref.Name()),
  )

  aes_message = getattr(
      messages, deploy_util.ResourceType.TARGET.value
  ).AssociatedEntitiesValue
  aes_value = aes_message()
  for key, value in associated_entities.items():
    aes_value.additionalProperties.append(
        aes_message.AdditionalProperty(key=key, value=value)
    )
  target.associatedEntities = aes_value


def SetCustomTarget(target, custom_target, project, region):
  """Sets the customTarget field of cloud deploy target message.

  This is handled specially because we allow providing either the ID or name for
  the custom target type referenced. When the ID is provided we need to
  construct the name.

  Args:
    target: googlecloudsdk.generated_clients.apis.clouddeploy.Target message.
    custom_target:
      googlecloudsdk.generated_clients.apis.clouddeploy.CustomTarget message.
    project: str, gcp project.
    region: str, ID of the location.
  """
  custom_target_type = custom_target.get('customTargetType')
  # If field contains '/' then we assume it's the name instead of the ID. No
  # adjustments required.
  if '/' in custom_target_type:
    target.customTarget = custom_target
    return

  custom_target_type_resource_ref = resources.REGISTRY.Parse(
      None,
      collection='clouddeploy.projects.locations.customTargetTypes',
      params={
          'projectsId': project,
          'locationsId': region,
          'customTargetTypesId': custom_target_type,
      },
  )
  custom_target['customTargetType'] = (
      custom_target_type_resource_ref.RelativeName()
  )
  target.customTarget = custom_target


def SetCustomTargetTasks(messages, custom_target_type, tasks):
  """Sets the tasks field of cloud deploy custom target type message.

  This is handled specially because we allow providing either the ID or name for
  the custom target type referenced. When the ID is provided we need to
  construct the name.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    custom_target_type: custom target type resource.
    tasks: googlecloudsdk.generated_clients.apis.clouddeploy.CustomTargetTasks
      message.
  """
  # Set any env variables defined on CustomTargetTasks.
  if tasks.get(DEPLOY_FIELD):
    SetEnvForTask(messages, tasks.get(DEPLOY_FIELD))
  if tasks.get(RENDER_FIELD):
    SetEnvForTask(messages, tasks.get(RENDER_FIELD))
  custom_target_type.tasks = tasks


def SetAutomationSelector(messages, automation, selectors):
  """Sets the selectors field of cloud deploy automation resource message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    automation:  googlecloudsdk.generated_clients.apis.clouddeploy.Automation
      message.
    selectors:
      [googlecloudsdk.generated_clients.apis.clouddeploy.TargetAttributes], list
      of TargetAttributes messages.
  """

  automation.selector = messages.AutomationResourceSelector()
  # check this first because it's the recommended format for selector.
  if not isinstance(selectors, list):
    for target_attribute in selectors.get(TARGETS_FIELD):
      _AddTargetAttribute(messages, automation.selector, target_attribute)
  else:
    for selector in selectors:
      message = selector.get(TARGET_FIELD)
      _AddTargetAttribute(messages, automation.selector, message)


def SetDeployPolicySelector(messages, deploy_policy, selectors):
  """Sets the selectors field of cloud deploy deploy policy resource message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    deploy_policy:
      googlecloudsdk.generated_clients.apis.clouddeploy.DeployPolicy message.
    selectors:
      [googlecloudsdk.generated_clients.apis.clouddeploy.DeployPolicyResourceSelector],
      list of DeployPolicyResourceSelector messages.
  """
  for selector in selectors:
    selector_message = messages.DeployPolicyResourceSelector()
    for field in selector:
      if field == DELIVERY_PIPELINE:
        dp_attribute_value = selector.get(field)
        dp_attribute_msg = messages.DeliveryPipelineAttribute()
        dp_attribute_populated = _SetSelectorAttributes(
            messages,
            dp_attribute_value,
            dp_attribute_msg,
            deploy_util.ResourceType.PIPELINE_ATTRIBUTE,
        )
        setattr(selector_message, DELIVERY_PIPELINE, dp_attribute_populated)
      if field == TARGET:
        target_attribute_value = selector.get(field)
        target_attribute_msg = messages.TargetAttribute()
        target_attribute_populated = _SetSelectorAttributes(
            messages,
            target_attribute_value,
            target_attribute_msg,
            deploy_util.ResourceType.TARGET_ATTRIBUTE,
        )
        setattr(selector_message, TARGET, target_attribute_populated)
    deploy_policy.selectors.append(selector_message)


def _SetSelectorAttributes(messages, attribute, attribute_msg, resource_type):
  """Sets the attribute message within a selector."""
  for field in attribute:
    if field == ID_FIELD:
      setattr(attribute_msg, field, attribute.get(field))
    if field == LABELS_FIELD:
      deploy_util.SetMetadata(
          messages,
          attribute_msg,
          resource_type,
          None,
          attribute.get(field),
      )
  return attribute_msg


def SetAutomationRules(messages, automation, automation_ref, rules):
  """Sets the rules field of cloud deploy automation resource message.

  Args:
    messages: module containing the definitions of messages for Cloud Deploy.
    automation:  googlecloudsdk.generated_clients.apis.clouddeploy.Automation
      message.
    automation_ref: protorpc.messages.Message, automation resource object.
    rules: [automation rule message], list of messages that are usd to create
      googlecloudsdk.generated_clients.apis.clouddeploy.AutomationRule messages.
  """
  _EnsureIsType(
      rules,
      list,
      'failed to parse automation {}, rules are defined incorrectly'.format(
          automation_ref.Name()
      ),
  )

  for rule in rules:
    automation_rule = messages.AutomationRule()
    if rule.get(PROMOTE_RELEASE_RULE_FIELD) or rule.get(PROMOTE_RELEASE_FIELD):
      message = rule.get(PROMOTE_RELEASE_RULE_FIELD) or rule.get(
          PROMOTE_RELEASE_FIELD
      )
      promote_release = messages.PromoteReleaseRule(
          id=message.get(ID_FIELD) or message.get(NAME_FIELD),
          wait=_WaitMinToSec(message.get(WAIT_FIELD)),
          destinationTargetId=message.get(DESTINATION_TARGET_ID_FIELD),
          destinationPhase=message.get(DESTINATION_PHASE_FIELD),
      )
      automation_rule.promoteReleaseRule = promote_release
    if rule.get(ADVANCE_ROLLOUT_RULE_FIELD) or rule.get(ADVANCE_ROLLOUT_FIELD):
      message = rule.get(ADVANCE_ROLLOUT_RULE_FIELD) or rule.get(
          ADVANCE_ROLLOUT_FIELD
      )
      advance_rollout = messages.AdvanceRolloutRule(
          id=message.get(ID_FIELD) or message.get(NAME_FIELD),
          wait=_WaitMinToSec(message.get(WAIT_FIELD)),
          sourcePhases=message.get(SOURCE_PHASES_FIELD) or [],
      )
      automation_rule.advanceRolloutRule = advance_rollout
    if rule.get(REPAIR_ROLLOUT_RULE_FIELD) or rule.get(REPAIR_ROLLOUT_FIELD):
      message = rule.get(REPAIR_ROLLOUT_RULE_FIELD) or rule.get(
          REPAIR_ROLLOUT_FIELD
      )
      automation_rule.repairRolloutRule = messages.RepairRolloutRule(
          id=message.get(ID_FIELD) or message.get(NAME_FIELD),
          phases=message.get(PHASES_FIELD) or [],
          jobs=message.get(JOBS_FIELD) or [],
          repairPhases=_ParseRepairPhases(
              messages, message.get(REPAIR_PHASES_FIELD) or []
          ),
      )
    if rule.get(TIMED_PROMOTE_RELEASE_RULE_FIELD):
      message = rule.get(TIMED_PROMOTE_RELEASE_RULE_FIELD)
      automation_rule.timedPromoteReleaseRule = (
          messages.TimedPromoteReleaseRule(
              id=message.get(ID_FIELD) or message.get(NAME_FIELD),
              schedule=message.get(SCHEDULE_FIELD),
              timeZone=message.get(TIME_ZONE_FIELD),
              destinationTargetId=message.get(DESTINATION_TARGET_ID_FIELD),
              destinationPhase=message.get(DESTINATION_PHASE_FIELD),
          )
      )
    automation.rules.append(automation_rule)


def ExportAutomationSelector(manifest, resource_selector):
  """Exports the selector field of the Automation resource.

  Args:
    manifest: A dictionary that represents the cloud deploy Automation resource.
    resource_selector:
      googlecloudsdk.generated_clients.apis.clouddeploy.AutomationResourceSelector
      message.
  """
  manifest[SELECTOR_FIELD] = []
  for selector in getattr(resource_selector, 'targets'):
    manifest[SELECTOR_FIELD].append({TARGET_FIELD: selector})


def ExportAutomationRules(manifest, rules):
  """Exports the selector field of the Automation resource.

  Args:
    manifest: A dictionary that represents the cloud deploy Automation resource.
    rules: [googlecloudsdk.generated_clients.apis.clouddeploy.AutomationRule],
      list of AutomationRule message.
  """
  manifest[RULES_FIELD] = []
  for rule in rules:
    resource = {}
    if getattr(rule, PROMOTE_RELEASE_RULE_FIELD):
      message = getattr(rule, PROMOTE_RELEASE_RULE_FIELD)
      promote = {}
      resource[PROMOTE_RELEASE_RULE_FIELD] = promote
      promote[ID_FIELD] = getattr(message, ID_FIELD)
      if getattr(message, DESTINATION_TARGET_ID_FIELD):
        promote[DESTINATION_TARGET_ID_FIELD] = getattr(
            message, DESTINATION_TARGET_ID_FIELD
        )
      if getattr(message, DESTINATION_PHASE_FIELD):
        promote[DESTINATION_PHASE_FIELD] = getattr(
            message, DESTINATION_PHASE_FIELD
        )
      if getattr(message, WAIT_FIELD):
        promote[WAIT_FIELD] = _WaitSecToMin(getattr(message, WAIT_FIELD))
    if getattr(rule, ADVANCE_ROLLOUT_RULE_FIELD):
      advance = {}
      resource[ADVANCE_ROLLOUT_RULE_FIELD] = advance
      message = getattr(rule, ADVANCE_ROLLOUT_RULE_FIELD)
      advance[ID_FIELD] = getattr(message, ID_FIELD)
      if getattr(message, SOURCE_PHASES_FIELD):
        advance[SOURCE_PHASES_FIELD] = getattr(message, SOURCE_PHASES_FIELD)
      if getattr(message, WAIT_FIELD):
        advance[WAIT_FIELD] = _WaitSecToMin(getattr(message, WAIT_FIELD))
    if getattr(rule, REPAIR_ROLLOUT_RULE_FIELD):
      repair = {}
      resource[REPAIR_ROLLOUT_RULE_FIELD] = repair
      message = getattr(rule, REPAIR_ROLLOUT_RULE_FIELD)
      repair[ID_FIELD] = getattr(message, ID_FIELD)
      if getattr(message, PHASES_FIELD):
        repair[PHASES_FIELD] = getattr(message, PHASES_FIELD)
      if getattr(message, JOBS_FIELD):
        repair[JOBS_FIELD] = getattr(message, JOBS_FIELD)
      if getattr(message, REPAIR_PHASES_FIELD):
        repair[REPAIR_PHASES_FIELD] = _ExportRepairPhases(
            getattr(message, REPAIR_PHASES_FIELD)
        )
    if getattr(rule, TIMED_PROMOTE_RELEASE_RULE_FIELD):
      message = getattr(rule, TIMED_PROMOTE_RELEASE_RULE_FIELD)
      timed_promote = {}
      resource[TIMED_PROMOTE_RELEASE_RULE_FIELD] = timed_promote
      timed_promote[ID_FIELD] = getattr(message, ID_FIELD)
      if getattr(message, SCHEDULE_FIELD):
        timed_promote[SCHEDULE_FIELD] = getattr(message, SCHEDULE_FIELD)
      if getattr(message, TIME_ZONE_FIELD):
        timed_promote[TIME_ZONE_FIELD] = getattr(message, TIME_ZONE_FIELD)
      if getattr(message, DESTINATION_TARGET_ID_FIELD):
        timed_promote[DESTINATION_TARGET_ID_FIELD] = getattr(
            message, DESTINATION_TARGET_ID_FIELD
        )
      if getattr(message, DESTINATION_PHASE_FIELD):
        timed_promote[DESTINATION_PHASE_FIELD] = getattr(
            message, DESTINATION_PHASE_FIELD
        )
    manifest[RULES_FIELD].append(resource)


def ExportDeployPolicyRules(manifest, rules):
  """Exports the policy rules field of the Deploy Policy resource.

  Args:
    manifest: A dictionary that represents the Deploy Policy rules resource.
    rules: [googlecloudsdk.generated_clients.apis.clouddeploy.PolicyRule], list
      of PolicyRule messages.
  """
  manifest['rules'] = []
  for rule in rules:
    if getattr(rule, 'rolloutRestriction'):
      rollout_restriction_rule = _ExportRolloutRestriction(rule)
      manifest['rules'].append(rollout_restriction_rule)


def _ExportRolloutRestriction(rule):
  """Exports the rolloutRestriction field of the Deploy Policy resource."""
  # This is a map of all the fields within the rolloutRestriction rule. It gets
  # populated as we parse the rolloutRestriction rule.
  rollout_restriction = {}
  # This is a map of rolloutRestriction: map of all the key/value pairs.
  rollout_restriction_rule = {}
  # Get the value of the rolloutRestriction rule.
  rollout_restriction_value = getattr(rule, 'rolloutRestriction')
  # id is required for the rolloutRestriction rule.
  rollout_restriction['id'] = rollout_restriction_value.id

  # Actions and invokers are optional for the rolloutRestriction rule. If not
  # specified, it implies all actions and all invokers are subject to the
  # Deploy Policy.
  if rollout_restriction_value.actions:
    rollout_restriction['actions'] = rollout_restriction_value.actions
  if rollout_restriction_value.invokers:
    rollout_restriction['invokers'] = rollout_restriction_value.invokers

  # Special handling needed for the timeWindows field. Specifically to
  # export the oneTimeWindows as a datetime string (e.g. '2024-01-01 00:00:00')
  # instead of the individual fields stored in the db. This is becuase a string
  # is much easier to read and adjust.
  time_windows_value = rollout_restriction_value.timeWindows
  # Populate the timeWindows field of the rolloutRestriction
  time_windows = {}
  _ExportTimeWindows(time_windows_value, time_windows)
  rollout_restriction['timeWindows'] = time_windows
  rollout_restriction_rule['rolloutRestriction'] = rollout_restriction
  return rollout_restriction_rule


def _ExportTimeWindows(time_windows_value, time_windows):
  """Exports the timeWindows field of the Deploy Policy resource."""
  time_windows['timeZone'] = time_windows_value.timeZone
  if time_windows_value.oneTimeWindows:
    _ExportOneTimeWindows(time_windows_value.oneTimeWindows, time_windows)
  if time_windows_value.weeklyWindows:
    _ExportWeeklyWindows(time_windows_value.weeklyWindows, time_windows)


def _ExportOneTimeWindows(one_time_windows, time_windows):
  """Exports the oneTimeWindows field of the Deploy Policy resource."""
  time_windows['oneTimeWindows'] = []
  for one_time_window in one_time_windows:
    one_time = {}
    start_date_time = _DateTimeIsoString(
        one_time_window.startDate, one_time_window.startTime
    )
    end_date_time = _DateTimeIsoString(
        one_time_window.endDate, one_time_window.endTime
    )
    one_time['start'] = start_date_time
    one_time['end'] = end_date_time
    time_windows['oneTimeWindows'].append(one_time)


def _DateTimeIsoString(date_obj, time_obj):
  """Converts a date and time to a string."""
  date_str = _FormatDate(date_obj)
  time_str = _FormatTime(time_obj)
  return f'{date_str} {time_str}'


def _FormatDate(date):
  """Converts a date object to a string."""
  return f'{date.year:04}-{date.month:02}-{date.day:02}'


def _ExportWeeklyWindows(weekly_windows, time_windows):
  """Exports the weeklyWindows field of the Deploy Policy resource."""
  time_windows['weeklyWindows'] = []
  for weekly_window in weekly_windows:
    weekly = {}
    start_time = weekly_window.startTime
    start = _FormatTime(start_time)
    weekly['startTime'] = start

    end_time = weekly_window.endTime
    end = _FormatTime(end_time)
    weekly['endTime'] = end
    if weekly_window.daysOfWeek:
      days = []
      for day in weekly_window.daysOfWeek:
        days.append(day.name)
      weekly['daysOfWeek'] = days
    time_windows['weeklyWindows'].append(weekly)


def _FormatTime(time_obj):
  """Converts a time object to a string."""
  hours = time_obj.hours or 0
  minutes = time_obj.minutes or 0
  time_str = f'{hours:02}:{minutes:02}'
  if time_obj.seconds or time_obj.nanos:
    seconds = time_obj.seconds or 0
    time_str += f':{seconds:02}'
  if time_obj.nanos:
    millis = time_obj.nanos / 1000000
    time_str += f'.{millis:03.0f}'
  return time_str


def _WaitMinToSec(wait):
  if not wait:
    return wait
  if not re.fullmatch(r'\d+m', wait):
    raise exceptions.AutomationWaitFormatError()
  mins = wait[:-1]
  # convert the minute to second
  seconds = int(mins) * 60
  return '%ss' % seconds


def _WaitSecToMin(wait):
  if not wait:
    return wait
  seconds = wait[:-1]
  # convert the minute to second
  mins = int(seconds) // 60
  return '%sm' % mins


def _EnsureIsType(value, t, msg):
  if not isinstance(value, t):
    raise exceptions.CloudDeployConfigError(msg)


def _ParseRepairPhases(messages, phases):
  """Parses RepairMode of the Automation resource."""
  modes_pb = []
  for p in phases:
    phase = messages.RepairPhaseConfig()
    if RETRY_FIELD in p:
      phase.retry = messages.Retry()
      retry = p.get(RETRY_FIELD)
      if retry:
        phase.retry.attempts = retry.get(ATTEMPTS_FIELD)
        phase.retry.wait = _WaitMinToSec(retry.get(WAIT_FIELD))
        phase.retry.backoffMode = _ParseBackoffMode(
            messages, retry.get(BACKOFF_MODE_FIELD)
        )
    if ROLLBACK_FIELD in p:
      phase.rollback = messages.Rollback()
      rollback = p.get(ROLLBACK_FIELD)
      if rollback:
        phase.rollback.destinationPhase = rollback.get(DESTINATION_PHASE_FIELD)
        phase.rollback.disableRollbackIfRolloutPending = rollback.get(
            DISABLE_ROLLBACK_IF_ROLLOUT_PENDING
        )
    modes_pb.append(phase)

  return modes_pb


def _ParseBackoffMode(messages, backoff):
  """Parses BackoffMode of the Automation resource."""
  if not backoff:
    return backoff
  if backoff in BACKOFF_CHOICES_SHORT:
    backoff = 'BACKOFF_MODE_' + backoff
  return arg_utils.ChoiceToEnum(
      backoff,
      messages.Retry.BackoffModeValueValuesEnum,
      valid_choices=BACKOFF_CHOICES_SHORT,
  )


def _ExportRepairPhases(repair_phases):
  """Exports RepairMode of the Automation resource."""
  phases = []
  for p in repair_phases:
    phase = {}
    if getattr(p, RETRY_FIELD):
      retry = {}
      phase[RETRY_FIELD] = retry
      message = getattr(p, RETRY_FIELD)
      if getattr(message, WAIT_FIELD):
        retry[WAIT_FIELD] = _WaitSecToMin(getattr(message, WAIT_FIELD))
      if getattr(message, ATTEMPTS_FIELD):
        retry[ATTEMPTS_FIELD] = getattr(message, ATTEMPTS_FIELD)
      if getattr(message, BACKOFF_MODE_FIELD):
        retry[BACKOFF_MODE_FIELD] = getattr(
            message, BACKOFF_MODE_FIELD
        ).name.split('_')[2]
    if getattr(p, ROLLBACK_FIELD):
      message = getattr(p, ROLLBACK_FIELD)
      rollback = {}
      phase[ROLLBACK_FIELD] = rollback
      if getattr(message, DESTINATION_PHASE_FIELD):
        rollback[DESTINATION_PHASE_FIELD] = getattr(
            message, DESTINATION_PHASE_FIELD
        )
      if getattr(message, DISABLE_ROLLBACK_IF_ROLLOUT_PENDING):
        rollback[DISABLE_ROLLBACK_IF_ROLLOUT_PENDING] = getattr(
            message, DISABLE_ROLLBACK_IF_ROLLOUT_PENDING
        )
    phases.append(phase)

  return phases


def _AddTargetAttribute(messages, resource_selector, message):
  """Add a new TargetAttribute to the resource selector resource."""
  target_attribute = messages.TargetAttribute()
  for field in message:
    value = message.get(field)
    if field == ID_FIELD:
      setattr(target_attribute, field, value)
    if field == LABELS_FIELD:
      deploy_util.SetMetadata(
          messages,
          target_attribute,
          deploy_util.ResourceType.TARGET_ATTRIBUTE,
          None,
          value,
      )
    resource_selector.targets.append(target_attribute)
