# -*- coding: utf-8 -*- #
# Copyright 2017 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.

"""The `app instances ssh` command."""

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

import textwrap

from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.app import appengine_api_client
from googlecloudsdk.api_lib.compute import base_classes as compute_base_classes
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.app import exceptions as command_exceptions
from googlecloudsdk.command_lib.app import flags
from googlecloudsdk.command_lib.app import iap_tunnel
from googlecloudsdk.command_lib.app import ssh_common
from googlecloudsdk.command_lib.util.ssh import ssh
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources


@base.ReleaseTracks(base.ReleaseTrack.GA)
@base.DefaultUniverseOnly
class ScpGa(base.Command):
  """SCP from or to the VM of an App Engine Flexible instance."""

  detailed_help = {
      'DESCRIPTION': textwrap.dedent("""\
        *{command}* lets you remotely copy files to or from an App Engine
        Flexible instance.""") + ssh_common.DETAILED_HELP,
      'EXAMPLES': """\
          To copy one file from a remote instance to the local machine, run:

              $ {command} --service=s1 --version=v1 i1:remote_file local_file

          To copy several local files to a remote instance, run:

              $ {command} --service=s1 --version=v1 local_1 local_1 i1:remote_dir

          To use recursive copy, run:

              $ {command} --service=s1 --version=v1 --recurse local_dir i1:remote_dir
          """,
  }

  @staticmethod
  def Args(parser):
    flags.AddServiceVersionSelectArgs(parser)
    iap_tunnel.AddSshTunnelArgs(parser)

    parser.add_argument(
        '--recurse',
        action='store_true',
        help='Upload directories recursively.')

    parser.add_argument(
        '--compress',
        action='store_true',
        help='Enable compression.')

    parser.add_argument(
        'sources',
        help='Specifies the files to copy.',
        metavar='[INSTANCE:]SRC',
        nargs='+')

    parser.add_argument(
        'destination',
        help='Specifies a destination for the source files.',
        metavar='[INSTANCE:]DEST')

  def Run(self, args):
    """Securily copy files from/to a running flex instance.

    Args:
      args: argparse.Namespace, the args the command was invoked with.

    Raises:
      InvalidInstanceTypeError: The instance is not supported for SSH.
      MissingVersionError: The version specified does not exist.
      MissingInstanceError: The instance specified does not exist.
      UnattendedPromptError: Not running in a tty.
      OperationCancelledError: User cancelled the operation.
      ssh.CommandError: The SCP command exited with SCP exit code, which
        usually implies that a connection problem occurred.

    Returns:
      int, The exit code of the SCP command.
    """
    api_client = appengine_api_client.GetApiClientForTrack(self.ReleaseTrack())
    env = ssh.Environment.Current()
    env.RequireSSH()
    keys = ssh.Keys.FromFilename()
    keys.EnsureKeysExist(overwrite=False)

    # Make sure we have a unique remote
    dst = ssh.FileReference.FromPath(args.destination)
    srcs = [ssh.FileReference.FromPath(source) for source in args.sources]
    ssh.SCPCommand.Verify(srcs, dst, single_remote=True)

    remote = dst.remote or srcs[0].remote
    instance = remote.host
    if not dst.remote:  # Make sure all remotes point to the same ref
      for src in srcs:
        src.remote = remote

    try:
      version_resource = api_client.GetVersionResource(args.service,
                                                       args.version)
    except apitools_exceptions.HttpNotFoundError:
      raise command_exceptions.MissingVersionError('{}/{}'.format(
          args.service, args.version))

    project = properties.VALUES.core.project.GetOrFail()
    res = resources.REGISTRY.Parse(
        instance,
        params={
            'appsId': project,
            'versionsId': args.version,
            'instancesId': instance,
            'servicesId': args.service,
        },
        collection='appengine.apps.services.versions.instances')
    instance_name = res.RelativeName()
    try:
      instance_resource = api_client.GetInstanceResource(res)
    except apitools_exceptions.HttpNotFoundError:
      raise command_exceptions.MissingInstanceError(instance_name)
    iap_tunnel_args = iap_tunnel.CreateSshTunnelArgs(
        args=args,
        api_client=api_client,
        track=self.ReleaseTrack(),
        project=project,
        version=version_resource,
        instance=instance_resource)

    region = '-'.join(instance_resource.vmZoneName.split('-')[:-1])
    user = ssh.GetDefaultSshUsername()
    project = ssh_common.GetComputeProject(self.ReleaseTrack())

    oslogin_state = ssh.GetOsloginState(
        None,
        project,
        user,
        keys.GetPublicKey().ToEntry(),
        None,
        self.ReleaseTrack(),
        app_engine_params={
            'appsId': project.name,
            'servicesId': res.servicesId,
            'versionsId': res.versionsId,
            'instancesId': instance,
            'serviceAccount': (
                version_resource.serviceAccount
                if version_resource.serviceAccount
                else ''
            ),
            'region': region
        },
        messages=compute_base_classes.ComputeApiHolder(
            self.ReleaseTrack()
        ).client.messages,
    )

    cert_file = None
    if oslogin_state.third_party_user or oslogin_state.require_certificates:
      cert_file = ssh.CertFileFromAppEngineInstance(
          project.name, res.servicesId, res.versionsId, instance
      )

    connection_details = ssh_common.PopulatePublicKey(
        api_client, args.service, args.version, remote.host,
        keys.GetPublicKey(), oslogin_state.user, oslogin_state.oslogin_enabled)

    # Update all remote references
    remote.host = connection_details.remote.host
    remote.user = connection_details.remote.user

    return ssh.SCPCommand(
        srcs,
        dst,
        identity_file=keys.key_file,
        cert_file=cert_file,
        compress=args.compress,
        recursive=args.recurse,
        options=connection_details.options,
        iap_tunnel_args=iap_tunnel_args).Run(env)


@base.ReleaseTracks(base.ReleaseTrack.BETA)
@base.DefaultUniverseOnly
class ScpBeta(base.Command):
  """SCP from or to the VM of an App Engine Flexible environment instance."""

  detailed_help = {
      'DESCRIPTION': textwrap.dedent("""\
        *{command}* lets you remotely copy files to or from an App Engine
        Flexible environment instance.""") + ssh_common.DETAILED_HELP,
      'EXAMPLES': """\
          To copy one file from a remote instance to the local machine, run:

              $ {command} --service=s1 --version=v1 i1:remote_file local_file

          To copy several local files to a remote instance, run:

              $ {command} --service=s1 --version=v1 local_1 local_1 i1:remote_dir

          To use recursive copy, run:

              $ {command} --service=s1 --version=v1 --recurse local_dir i1:remote_dir
          """,
  }

  @staticmethod
  def Args(parser):
    flags.AddServiceVersionSelectArgs(parser)
    iap_tunnel.AddSshTunnelArgs(parser)

    parser.add_argument(
        '--recurse',
        action='store_true',
        help='Upload directories recursively.')

    parser.add_argument(
        '--compress',
        action='store_true',
        help='Enable compression.')

    parser.add_argument(
        'sources',
        help='Specifies the files to copy.',
        metavar='[INSTANCE:]SRC',
        nargs='+')

    parser.add_argument(
        'destination',
        help='Specifies a destination for the source files.',
        metavar='[INSTANCE:]DEST')

  def Run(self, args):
    """Securily copy files from/to a running flex instance.

    Args:
      args: argparse.Namespace, the args the command was invoked with.

    Raises:
      InvalidInstanceTypeError: The instance is not supported for SSH.
      MissingVersionError: The version specified does not exist.
      MissingInstanceError: The instance specified does not exist.
      UnattendedPromptError: Not running in a tty.
      OperationCancelledError: User cancelled the operation.
      ssh.CommandError: The SCP command exited with SCP exit code, which
        usually implies that a connection problem occurred.

    Returns:
      int, The exit code of the SCP command.
    """
    api_client = appengine_api_client.GetApiClientForTrack(self.ReleaseTrack())
    env = ssh.Environment.Current()
    env.RequireSSH()
    keys = ssh.Keys.FromFilename()
    keys.EnsureKeysExist(overwrite=False)

    # Make sure we have a unique remote
    dst = ssh.FileReference.FromPath(args.destination)
    srcs = [ssh.FileReference.FromPath(source) for source in args.sources]
    ssh.SCPCommand.Verify(srcs, dst, single_remote=True)

    remote = dst.remote or srcs[0].remote
    instance = remote.host
    if not dst.remote:  # Make sure all remotes point to the same ref
      for src in srcs:
        src.remote = remote

    try:
      version_resource = api_client.GetVersionResource(args.service,
                                                       args.version)
    except apitools_exceptions.HttpNotFoundError:
      raise command_exceptions.MissingVersionError('{}/{}'.format(
          args.service, args.version))

    project = properties.VALUES.core.project.GetOrFail()
    res = resources.REGISTRY.Parse(
        instance,
        params={
            'appsId': project,
            'versionsId': args.version,
            'instancesId': instance,
            'servicesId': args.service,
        },
        collection='appengine.apps.services.versions.instances')
    instance_name = res.RelativeName()
    try:
      instance_resource = api_client.GetInstanceResource(res)
    except apitools_exceptions.HttpNotFoundError:
      raise command_exceptions.MissingInstanceError(instance_name)
    iap_tunnel_args = iap_tunnel.CreateSshTunnelArgs(args, api_client,
                                                     self.ReleaseTrack(),
                                                     project, version_resource,
                                                     instance_resource)

    region = '-'.join(instance_resource.vmZoneName.split('-')[:-1])
    user = ssh.GetDefaultSshUsername()
    project = ssh_common.GetComputeProject(self.ReleaseTrack())

    oslogin_state = ssh.GetOsloginState(
        None,
        project,
        user,
        keys.GetPublicKey().ToEntry(),
        None,
        self.ReleaseTrack(),
        app_engine_params={
            'appsId': project.name,
            'servicesId': res.servicesId,
            'versionsId': res.versionsId,
            'instancesId': instance,
            'serviceAccount': (
                version_resource.serviceAccount
                if version_resource.serviceAccount
                else ''
            ),
            'region': region
        },
        messages=compute_base_classes.ComputeApiHolder(
            self.ReleaseTrack()
        ).client.messages,
    )

    cert_file = None
    if oslogin_state.third_party_user or oslogin_state.require_certificates:
      cert_file = ssh.CertFileFromAppEngineInstance(
          project.name, res.servicesId, res.versionsId, instance
      )
    connection_details = ssh_common.PopulatePublicKey(
        api_client, args.service, args.version, remote.host,
        keys.GetPublicKey(), oslogin_state.user, oslogin_state.oslogin_enabled)

    # Update all remote references
    remote.host = connection_details.remote.host
    remote.user = connection_details.remote.user

    cmd = ssh.SCPCommand(
        srcs,
        dst,
        identity_file=keys.key_file,
        cert_file=cert_file,
        compress=args.compress,
        recursive=args.recurse,
        options=connection_details.options,
        iap_tunnel_args=iap_tunnel_args)
    return cmd.Run(env)

