# -*- coding: utf-8 -*- #
# Copyright 2026 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.
"""Create a release."""


import datetime
import os.path

from googlecloudsdk.api_lib.clouddeploy import client_util
from googlecloudsdk.api_lib.clouddeploy import release
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as c_exceptions
from googlecloudsdk.command_lib.deploy import delivery_pipeline_util
from googlecloudsdk.command_lib.deploy import deploy_policy_util
from googlecloudsdk.command_lib.deploy import deploy_util
from googlecloudsdk.command_lib.deploy import flags
from googlecloudsdk.command_lib.deploy import promote_util
from googlecloudsdk.command_lib.deploy import release_util
from googlecloudsdk.command_lib.deploy import resource_args
from googlecloudsdk.core import exceptions as core_exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import times


_DETAILED_HELP = {
    'DESCRIPTION': '{description}',
    'EXAMPLES': """ \
    To create a release with source located at storage URL `gs://bucket/object.zip`
    and the first rollout in the first target of the promotion sequence:

       $ {command} my-release --source=`gs://bucket/object.zip` --delivery-pipeline=my-pipeline --region=us-central1

    To create a release with source located at current directory
    and deploy a rollout to target prod :

      $ {command} my-release --delivery-pipeline=my-pipeline --region=us-central1 --to-target=prod

    The following command creates a release without a `skaffold.yaml` as input, and generates one
    for you:

      $ {command} my-release --delivery-pipeline=my-pipeline --region=us-central1 --from-k8s-manifest=path/to/kubernetes/k8.yaml

    The current UTC date and time on the machine running the gcloud command can
    also be included in the release name by adding $DATE and $TIME parameters:

      $ {command} 'my-release-$DATE-$TIME' --delivery-pipeline=my-pipeline --region=us-central1

    If the current UTC date and time is set to 2021-12-21 12:02, then the created release
    will have its name set as my-release-20211221-1202.

    When using these parameters, please be sure to wrap the release name in single quotes
    or else the template parameters will be overridden by environment variables.
    """,
}
_RELEASE = 'release'
_MAINTENANCE_WARNING_DAYS = 28


def _CommonArgs(parser):
  """Register flags for this command.

  Args:
    parser: An argparse.ArgumentParser-like object. It is mocked out in order to
      capture some information, but behaves like an ArgumentParser.
  """
  resource_args.AddReleaseResourceArg(parser, positional=True, required=True)
  flags.AddGcsSourceStagingDirFlag(parser)
  flags.AddImagesGroup(parser)
  flags.AddIgnoreFileFlag(parser)
  flags.AddToTargetFlag(parser)
  flags.AddDescription(parser, 'Description of the release.')
  flags.AddAnnotationsFlag(parser, _RELEASE)
  flags.AddLabelsFlag(parser, _RELEASE)
  flags.AddDockerVersion(parser)
  flags.AddHelmVersion(parser)
  flags.AddKptVersion(parser)
  flags.AddKubectlVersion(parser)
  flags.AddKustomizeVersion(parser)
  flags.AddSkaffoldVersion(parser)
  flags.AddSkaffoldOrNativeSources(parser)
  flags.AddInitialRolloutGroup(parser)
  flags.AddDeployParametersFlag(parser)
  flags.AddOverrideDeployPolicies(parser)


@base.ReleaseTracks(
    base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA, base.ReleaseTrack.GA
)
@base.DefaultUniverseOnly
class Create(base.CreateCommand):
  """Creates a new release, delivery pipeline qualified."""

  detailed_help = _DETAILED_HELP

  @staticmethod
  def Args(parser):
    _CommonArgs(parser)

  def _CheckIfNearMaintenance(self, release_obj):
    """Checks to see if a release is close to the maintenance window."""

    def _ParseDt(dt):
      """Parses the maintenance dt, returning a datetime or None."""
      if dt is None:
        return None
      try:
        return times.ParseDateTime(dt)
      except (times.DateTimeSyntaxError, times.DateTimeValueError):
        return None

    release_condition = release_obj.condition
    if release_condition is None:
      return

    has_tool_versions = (
        release_condition.dockerVersionSupportedCondition
        or release_condition.helmVersionSupportedCondition
        or release_condition.kptVersionSupportedCondition
        or release_condition.kubectlVersionSupportedCondition
        or release_condition.kustomizeVersionSupportedCondition
        or release_condition.skaffoldVersionSupportedCondition
    )
    if not has_tool_versions:
      if release_condition.skaffoldSupportedCondition is None:
        return
      maintenance_dt = _ParseDt(
          release_condition.skaffoldSupportedCondition.maintenanceModeTime
      )
      # It is possible to have no maintenance mode time.
      # Like `skaffold_preview` for example.
      if (
          maintenance_dt is not None
          and maintenance_dt - times.Now()
          <= datetime.timedelta(days=_MAINTENANCE_WARNING_DAYS)
      ):
        log.status.Print(
            "WARNING: This release's Skaffold version will be"
            ' in maintenance mode beginning on {date}.'
            " After that you won't be able to create releases"
            ' using this version of Skaffold.\n'
            'https://cloud.google.com/deploy/docs/using-skaffold'
            '/select-skaffold#skaffold_version_deprecation'
            '_and_maintenance_policy'.format(
                date=maintenance_dt.strftime('%Y-%m-%d')
            )
        )
      return

    # use an array to have deterministic ordering.
    tools_supported_condition_to_process = [
        (
            release_util.Tools.DOCKER,
            release_condition.dockerVersionSupportedCondition,
        ),
        (
            release_util.Tools.HELM,
            release_condition.helmVersionSupportedCondition,
        ),
        (
            release_util.Tools.KPT,
            release_condition.kptVersionSupportedCondition,
        ),
        (
            release_util.Tools.KUBECTL,
            release_condition.kubectlVersionSupportedCondition,
        ),
        (
            release_util.Tools.KUSTOMIZE,
            release_condition.kustomizeVersionSupportedCondition,
        ),
        (
            release_util.Tools.SKAFFOLD,
            release_condition.skaffoldVersionSupportedCondition,
        ),
    ]
    tools_almost_in_maintenance = []
    for tool, condition in tools_supported_condition_to_process:
      if not condition:
        continue
      maintenance_dt = _ParseDt(condition.maintenanceModeTime)
      if (
          maintenance_dt is not None
          and maintenance_dt - times.Now()
          <= datetime.timedelta(days=_MAINTENANCE_WARNING_DAYS)
      ):
        tools_almost_in_maintenance.append(tool)
    if tools_almost_in_maintenance:
      joined = ', '.join([tool.value for tool in tools_almost_in_maintenance])
      log.status.Print(
          f'WARNING: The versions used for tools: [{joined}] will be in'
          " maintenance mode soon. After that you won't be able to create"
          ' releases using these versions of the tools.\n'
          'https://cloud.google.com/deploy/docs/select-tool-version'
      )

  def Run(self, args):
    """This is what gets called when the user runs this command.

    Args:
      args: All the arguments that were provided to this command invocation.

    Returns:
      The release and rollout created.
    """
    if args.disable_initial_rollout and args.to_target:
      raise c_exceptions.ConflictingArgumentsException(
          '--disable-initial-rollout', '--to-target'
      )

    args.CONCEPTS.parsed_args.release = release_util.RenderPattern(
        args.CONCEPTS.parsed_args.release
    )
    release_ref = args.CONCEPTS.release.Parse()
    pipeline_obj = delivery_pipeline_util.GetPipeline(
        release_ref.Parent().RelativeName()
    )
    failed_activity_msg = 'Cannot create release {}.'.format(
        release_ref.RelativeName()
    )
    delivery_pipeline_util.ThrowIfPipelineSuspended(
        pipeline_obj, failed_activity_msg
    )
    actual_source, native_source_specified = release_util.GetActualSource(args)
    # Only when the skaffold file is an absolute path needs to be handled
    # here.
    if args.skaffold_file and os.path.isabs(args.skaffold_file):
      # If source is not specified, use current directory.
      # The source is only used for the file path check, it's not used in the
      # CreateReleaseConfig.
      source_description = (
          'current working directory' if args.source == '.' else 'source'
      )
      if not files.IsDirAncestorOf(actual_source, args.skaffold_file):
        raise core_exceptions.Error(
            f'The skaffold file {args.skaffold_file} could not be found in the'
            f' {source_description}. Please enter a valid Skaffold file path.'
        )
      args.skaffold_file = os.path.relpath(
          os.path.abspath(args.skaffold_file), os.path.abspath(actual_source)
      )
    config_check_mode = release_util.GetConfigCheckModeFromArgs(args)
    native_config_used = release_util.VerifyConfigs(
        actual_source, config_check_mode, args.skaffold_file, args.config_file
    )

    client = release.ReleaseClient()
    # Create the release create request.
    release_config = release_util.CreateReleaseConfig(
        source=actual_source,
        gcs_source_staging_dir=args.gcs_source_staging_dir,
        ignore_file=args.ignore_file,
        images=args.images,
        build_artifacts=args.build_artifacts,
        description=args.description,
        docker_version=args.docker_version,
        helm_version=args.helm_version,
        kpt_version=args.kpt_version,
        kubectl_version=args.kubectl_version,
        kustomize_version=args.kustomize_version,
        skaffold_version=args.skaffold_version,
        skaffold_file=args.skaffold_file,
        location=release_ref.AsDict()['locationsId'],
        pipeline_uuid=pipeline_obj.uid,
        from_k8s_manifest=args.from_k8s_manifest,
        from_run_manifest=args.from_run_manifest,
        pipeline_obj=pipeline_obj,
        deploy_parameters=args.deploy_parameters,
        config_file=args.config_file,
        native_config_used=native_config_used,
        native_source_specified=native_source_specified,
    )

    deploy_util.SetMetadata(
        client.messages,
        release_config,
        deploy_util.ResourceType.RELEASE,
        args.annotations,
        args.labels,
    )
    operation = client.Create(release_ref, release_config)
    operation_ref = resources.REGISTRY.ParseRelativeName(
        operation.name, collection='clouddeploy.projects.locations.operations'
    )
    client_util.OperationsClient().WaitForOperation(operation, operation_ref)

    log.status.Print(
        'Created Cloud Deploy release {}.'.format(release_ref.Name())
    )

    release_obj = release.ReleaseClient().Get(release_ref.RelativeName())
    self._CheckIfNearMaintenance(release_obj)
    if args.disable_initial_rollout:
      return release_obj
    # On the command line deploy policy IDs are provided, but for the
    # CreateRollout API we need to provide the full resource name.
    pipeline_ref = release_ref.Parent()
    policies = deploy_policy_util.CreateDeployPolicyNamesFromIDs(
        pipeline_ref, args.override_deploy_policies
    )
    rollout_resource = promote_util.Promote(
        release_ref,
        release_obj,
        args.to_target,
        is_create=True,
        labels=args.initial_rollout_labels,
        annotations=args.initial_rollout_annotations,
        starting_phase_id=args.initial_rollout_phase_id,
        override_deploy_policies=policies,
    )

    return release_obj, rollout_resource
