# -*- coding: utf-8 -*- #
# Copyright 2013 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.
"""Clones a Cloud SQL instance."""


from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.sql import api_util
from googlecloudsdk.api_lib.sql import exceptions
from googlecloudsdk.api_lib.sql import operations
from googlecloudsdk.api_lib.sql import validate
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.sql import flags
from googlecloudsdk.command_lib.sql import instances as command_util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources

DESCRIPTION = """\

    *{command}* creates a clone of a Cloud SQL instance. The clone is an
    independent copy of the source instance with the same data and settings.
    Source and destination instances must be in the same project. An instance
    can be cloned from its current state, or from an earlier point in time.

    For MySQL: The binary log coordinates or timestamp (point in time), if
    specified, act as the point in time the source instance is cloned from. If
    not specified, the current state of the instance is cloned.

    For PostgreSQL: The point in time, if specified, defines a past state of the
    instance to clone. If not specified, the current state of the instance is
    cloned.

    For SQL Server: The point in time, if specified, defines a past state of the
    instance to clone. If not specified, the current state of the instance is
    cloned.

    """

EXAMPLES_GA = """\
    To clone an instance from its current state (most recent binary log
  coordinates):

    $ {command} my-source-instance my-cloned-instance

  To clone a MySQL instance from an earlier point in time (past binary log
  coordinates):

    $ {command} my-source-instance my-cloned-instance --bin-log-file-name mysql-bin.000020 --bin-log-position 170

  To clone a MySQL source instance at a specific point in time:

    $ {command} my-source-instance my-cloned-instance --point-in-time '2012-11-15T16:19:00.094Z'

  To clone a PostgreSQL source instance at a specific point in time:

    $ {command} my-source-instance my-cloned-instance --point-in-time '2012-11-15T16:19:00.094Z'

  To clone a SQL Server source instance at a specific point in time:

    $ {command} my-source-instance my-cloned-instance --point-in-time '2012-11-15T16:19:00.094Z'

  To clone a deleted instance, include the name and deletion time of the source instance:

  $ {command} my-source-instance my-cloned-instance --source-instance-deletion-time '2012-11-15T16:19:00.094Z'
    """

EXAMPLES_ALPHA = """\

  To specify the allocated IP range for the private IP target Instance
  (reserved for future use):

  $ {command} my-source-instance my-cloned-instance --allocated-ip-range-name cloned-instance-ip-range
    """

DETAILED_HELP = {
    'DESCRIPTION': DESCRIPTION,
    'EXAMPLES': EXAMPLES_GA,
}

DETAILED_APLHA_HELP = {
    'DESCRIPTION': DESCRIPTION,
    'EXAMPLES': EXAMPLES_GA + EXAMPLES_ALPHA,
}


def _GetInstanceRefsFromArgs(args, client):
  """Get validated refs to source and destination instances from args."""

  if bool(args.destination_project) != bool(args.destination_network):
    raise exceptions.ArgumentError(
        'Both --destination-project and --destination-network must be '
        'specified for a cross-project clone.'
    )

  validate.ValidateInstanceName(args.source)
  validate.ValidateInstanceName(args.destination)
  source_instance_ref = client.resource_parser.Parse(
      args.source,
      params={'project': properties.VALUES.core.project.GetOrFail},
      collection='sql.instances',
  )
  if args.destination_project:
    destination_instance_ref = client.resource_parser.Parse(
        args.destination,
        params={'project': args.destination_project},
        collection='sql.instances',
    )
  else:
    destination_instance_ref = client.resource_parser.Parse(
        args.destination,
        params={'project': properties.VALUES.core.project.GetOrFail},
        collection='sql.instances',
    )
    _CheckSourceAndDestinationProjectsMatch(
        source_instance_ref, destination_instance_ref
    )
  return source_instance_ref, destination_instance_ref


def _CheckSourceAndDestinationProjectsMatch(
    source_instance_ref: resources.Resource,
    destination_instance_ref: resources.Resource,
) -> None:
  """Verify that the source and destination projects are the same."""

  if source_instance_ref.project != destination_instance_ref.project:
    raise exceptions.ArgumentError(
        'The source and the clone instance must belong to the same project:'
        f' "{source_instance_ref.project}" !='
        f' "{destination_instance_ref.project}".'
    )


def AddAlphaArgs(parser):
  """Declare alpha flags for this command parser."""
  parser.add_argument(
      '--allocated-ip-range-name',
      required=False,
      help="""\
      The name of the IP range allocated for the destination instance with
      private network connectivity. For example:
      \'google-managed-services-default\'. If set, the destination instance
      IP is created in the allocated range represented by this name.
      Reserved for future use.
      """,
  )


def _UpdateRequestFromArgs(request, args, sql_messages, release_track):
  """Update request with clone options."""
  clone_context = request.instancesCloneRequest.cloneContext
  # PITR options
  if args.bin_log_file_name and args.bin_log_position:
    clone_context.binLogCoordinates = sql_messages.BinLogCoordinates(
        binLogFileName=args.bin_log_file_name,
        binLogPosition=args.bin_log_position,
    )
  elif args.point_in_time:
    clone_context.pointInTime = args.point_in_time.strftime(
        '%Y-%m-%dT%H:%M:%S.%fZ'
    )

  if args.point_in_time and args.restore_database_name:
    clone_context.databaseNames[:] = [args.restore_database_name]

  if args.preferred_zone:
    clone_context.preferredZone = args.preferred_zone

  if args.preferred_secondary_zone:
    clone_context.preferredSecondaryZone = args.preferred_secondary_zone

  if args.destination_project:
    clone_context.destinationProject = args.destination_project

  if args.destination_network:
    clone_context.destinationNetwork = args.destination_network

  if args.source_instance_deletion_time:
    clone_context.sourceInstanceDeletionTime = (
        args.source_instance_deletion_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
    )

  if release_track == base.ReleaseTrack.ALPHA:
    # ALLOCATED IP RANGE options
    if args.allocated_ip_range_name:
      clone_context.allocatedIpRange = args.allocated_ip_range_name


def RunBaseCloneCommand(args, release_track):
  """Clones a Cloud SQL instance.

  Args:
    args: argparse.Namespace, The arguments used to invoke this command.
    release_track: base.ReleaseTrack, the release track that this was run under.

  Returns:
    A dict object representing the operations resource describing the
    clone operation if the clone was successful.
  Raises:
    ArgumentError: The arguments are invalid for some reason.
  """

  client = api_util.SqlClient(api_util.API_VERSION_DEFAULT)
  sql_client = client.sql_client
  sql_messages = client.sql_messages

  source_instance_ref, destination_instance_ref = _GetInstanceRefsFromArgs(
      args, client
  )

  request = sql_messages.SqlInstancesCloneRequest(
      project=source_instance_ref.project,
      instance=source_instance_ref.instance,
      instancesCloneRequest=sql_messages.InstancesCloneRequest(
          cloneContext=sql_messages.CloneContext(
              kind='sql#cloneContext',
              destinationInstanceName=destination_instance_ref.instance,
          )
      ),
  )

  _UpdateRequestFromArgs(request, args, sql_messages, release_track)

  # Check if source has customer-managed key; show warning if so.
  try:
    source_instance_resource = sql_client.instances.Get(
        sql_messages.SqlInstancesGetRequest(
            project=source_instance_ref.project,
            instance=source_instance_ref.instance,
        )
    )
    if source_instance_resource.diskEncryptionConfiguration:
      command_util.ShowCmekWarning('clone', 'the source instance')
  except apitools_exceptions.HttpError:
    # This is for informational purposes, so don't throw an error if failure.
    pass

  result = sql_client.instances.Clone(request)

  operation_ref = client.resource_parser.Create(
      'sql.operations',
      operation=result.name,
      project=destination_instance_ref.project,
  )

  if args.async_:
    if not args.IsSpecified('format'):
      args.format = 'default'
    return sql_client.operations.Get(
        sql_messages.SqlOperationsGetRequest(
            project=operation_ref.project, operation=operation_ref.operation
        )
    )
  operations.OperationsV1Beta4.WaitForOperation(
      sql_client, operation_ref, 'Cloning Cloud SQL instance'
  )
  log.CreatedResource(destination_instance_ref)
  rsource = sql_client.instances.Get(
      sql_messages.SqlInstancesGetRequest(
          project=destination_instance_ref.project,
          instance=destination_instance_ref.instance,
      )
  )
  rsource.kind = None
  return rsource


def AddBaseArgs(parser):
  """Add args common to all release tracks to parser."""
  base.ASYNC_FLAG.AddToParser(parser)
  parser.display_info.AddFormat(flags.GetInstanceListFormat())
  parser.add_argument(
      'source',
      completer=flags.InstanceCompleter,
      help='Cloud SQL instance ID of the source.',
  )
  parser.add_argument('destination', help='Cloud SQL instance ID of the clone.')
  parser.add_argument(
      '--destination-project',
      required=False,
      help="""\
      The project ID of the destination project where the cloned instance
      will be created. To perform a cross-project clone, this field is
      required. If not specified, the clone is created in the same project
      as the source instance.
      """,
      hidden=True,
  )
  parser.add_argument(
      '--destination-network',
      required=False,
      help="""\
      The fully qualified URI of the VPC network to which the cloned instance
      will be connected via Private Services Access for private IP. For
      example: `projects/my-network-project/global/networks/my-network`. This
      field is required only for cross-project cloning.
      """,
      hidden=True,
  )
  pitr_options_group = parser.add_group(mutex=True, required=False)
  bin_log_group = pitr_options_group.add_group(
      mutex=False,
      required=False,
      help='Binary log coordinates for point-in-time recovery.',
  )
  bin_log_group.add_argument(
      '--bin-log-file-name',
      required=True,
      help="""\
      The name of the binary log file. Enable point-in-time recovery on the
      source instance to create a binary log file. If specified with
      <--bin-log-position> to form a valid binary log coordinate, it defines an
      earlier point in time to clone a source instance from.
      For example, mysql-bin.000001.
      """,
  )
  bin_log_group.add_argument(
      '--bin-log-position',
      type=int,
      required=True,
      help="""\
      Represents the state of an instance at any given point in time inside a
      binary log file. If specified along with <--bin-log-file-name> to form a
      valid binary log coordinate, it defines an earlier point in time to clone
      a source instance from.
      For example, 123 (a numeric value).
      """,
  )
  point_in_time_group = pitr_options_group.add_group(
      mutex=False, required=False
  )
  point_in_time_group.add_argument(
      '--point-in-time',
      type=arg_parsers.Datetime.Parse,
      required=True,
      help="""\
      Represents the state of an instance at any given point in time inside
      a transaction log file. For MySQL, the binary log file is used for
      transaction logs. For PostgreSQL, the write-ahead log file is used for
      transaction logs. For SQL Server, the log backup file is used for
      such purpose. To create a transaction log, enable point-in-time recovery
      on the source instance. Instance should have transaction logs accumulated
      up to the point in time they want to restore up to. Uses RFC 3339 format
      in UTC timezone. If specified, defines a past state of the instance to
      clone. For example, '2012-11-15T16:19:00.094Z'.
      """,
  )
  point_in_time_group.add_argument(
      '--restore-database-name',
      required=False,
      help="""\
    The name of the database to be restored for a point-in-time restore. If
    set, the destination instance will only restore the specified database.
    """,
  )
  parser.add_argument(
      '--preferred-zone',
      required=False,
      help="""\
    The preferred zone for the cloned instance. If you specify a value for
    this flag, then the destination instance uses the value as the primary
    zone.
    """,
  )
  parser.add_argument(
      '--preferred-secondary-zone',
      required=False,
      help="""\
    The preferred secondary zone for the cloned regional instance. If you
    specify a value for this flag, then the destination instance uses the
    value as the secondary zone. The secondary zone can't be the same as the
    primary zone.
    """,
  )
  parser.add_argument(
      '--source-instance-deletion-time',
      type=arg_parsers.Datetime.Parse,
      required=False,
      help="""\
      The time the source instance was deleted. This is required if cloning
      from a deleted instance.
      """,
  )


@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.GA, base.ReleaseTrack.BETA)
class Clone(base.CreateCommand):
  """Clones a Cloud SQL instance."""

  detailed_help = DETAILED_HELP

  @classmethod
  def Args(cls, parser):
    """Declare flag and positional arguments for the command parser."""
    AddBaseArgs(parser)
    parser.display_info.AddCacheUpdater(flags.InstanceCompleter)

  def Run(self, args):
    return RunBaseCloneCommand(args, self.ReleaseTrack())


@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class CloneAlpha(base.CreateCommand):
  """Clones a Cloud SQL instance."""

  detailed_help = DETAILED_APLHA_HELP

  def Run(self, args):
    return RunBaseCloneCommand(args, self.ReleaseTrack())

  @staticmethod
  def Args(parser):
    """Args is called by calliope to gather arguments for this command."""
    AddBaseArgs(parser)
    AddAlphaArgs(parser)
    parser.display_info.AddCacheUpdater(flags.InstanceCompleter)
