# -*- 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 __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

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 instances as instance_util
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


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

  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')
  destination_instance_ref = client.resource_parser.Parse(
      args.destination,
      params={'project': properties.VALUES.core.project.GetOrFail},
      collection='sql.instances')

  _CheckSourceAndDestination(source_instance_ref, destination_instance_ref)
  return source_instance_ref, destination_instance_ref


def _CheckSourceAndDestination(source_instance_ref, destination_instance_ref):
  """Verify that the source and destination instance ids are different."""

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


def _UpdateRequestFromArgs(request, args, sql_messages):
  """Update request with point-in-time recovery options."""

  if args.bin_log_file_name and args.bin_log_position:
    clone_context = request.instancesCloneRequest.cloneContext
    clone_context.binLogCoordinates = sql_messages.BinLogCoordinates(
        binLogFileName=args.bin_log_file_name,
        binLogPosition=args.bin_log_position)
  elif args.point_in_time:
    clone_context = request.instancesCloneRequest.cloneContext
    clone_context.pointInTime = args.point_in_time.strftime(
        '%Y-%m-%dT%H:%M:%S.%fZ')


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

  Args:
    args: argparse.Namespace, The arguments used to invoke this command.

  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)

  # Check if source is V1; raise error if so.
  # 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))

    # TODO(b/122660263): Remove when V1 instances are no longer supported.
    if instance_util.IsInstanceV1(sql_messages, source_instance_resource):
      raise exceptions.ArgumentError(
          'First Generation instances can no longer be created.')
    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.')

  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).
      """)
  pitr_options_group.add_argument(
      '--point-in-time',
      type=arg_parsers.Datetime.Parse,
      required=False,
      help="""\
      Represents the state of an instance at any given point in time inside a
      write-ahead log file. Enable point-in-time recovery on the source
      instance to create a write-ahead log file. 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'.
      """)


@base.ReleaseTracks(base.ReleaseTrack.GA, base.ReleaseTrack.BETA,
                    base.ReleaseTrack.ALPHA)
class Clone(base.CreateCommand):
  # pylint: disable=line-too-long
  """Clones a Cloud SQL instance.

  *{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, 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.

  ## EXAMPLES

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

    $ {command} instance-foo instance-bar

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

    $ {command} instance-foo instance-bar --bin-log-file-name mysql-bin.000020 --bin-log-position 170

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

    $ {command} instance-foo instance-bar --point-in-time '2012-11-15T16:19:00.094Z'
  """

  @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)
