# Copyright 2013 Google Inc. All Rights Reserved.

"""Common bootstrapping functionality used by the wrapper scripts."""

# Disables import order warning and unused import.  Setup changes the python
# path so cloud sdk imports will actually work, so it must come first.
# pylint: disable=C6203
# pylint: disable=W0611
from __future__ import absolute_import
from __future__ import unicode_literals

# Python 3 is strict about imports and we use this file in different ways, which
# makes sub-imports difficult. In general, when a script is executed, that
# directory is put on the PYTHONPATH. The issue is that some of the wrapper
# scripts are executed from within the bootstrapping/ directory and some are
# executed from within the bin/ directory.
# pylint: disable=g-statement-before-imports
if '.' in __name__:
  # Here, __name__ will be bootstrapping.bootstrapping. This indicates that this
  # file was loaded as a member of package bootstrapping. This in turn indicates
  # that the main file that was executed was not in the bootstrapping directory,
  # so bin/ is on the path and bootstrapping is considered a python package.
  # Do an import of setup from this current package.
  from . import setup  # pylint:disable=g-import-not-at-top
else:
  # In this case, __name__ is bootstrapping, which indicates that the main
  # script was executed from within this directory meaning that Python doesn't
  # consider this a package but rather the root of the PYTHONPATH. We can't do
  # the above import because since we are not in a package, the '.' doesn't
  # refer to anything. Just do a direct import which will find setup on the
  # PYTHONPATH (which is just this directory).
  import setup  # pylint:disable=g-import-not-at-top

import gcloud
import sys
# Reorder sys.path if needed right now before more modules loaded and cached
sys.path = gcloud.reorder_sys_path(sys.path)

# pylint: disable=g-import-not-at-top
import json
# pylint: enable=g-import-not-at-top
import os
import platform

from googlecloudsdk.core import config
from googlecloudsdk.core import execution_utils
from googlecloudsdk.core import metrics
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.credentials import store as c_store
from googlecloudsdk.core.updater import local_state
from googlecloudsdk.core.updater import update_manager
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import platforms
from six.moves import input


BOOTSTRAPPING_DIR = os.path.dirname(os.path.realpath(__file__))
BIN_DIR = os.path.dirname(BOOTSTRAPPING_DIR)
SDK_ROOT = os.path.dirname(BIN_DIR)


def DisallowIncompatiblePythonVersions():
  if not platforms.PythonVersion().IsCompatible():
    sys.exit(1)


def GetDecodedArgv():
  return [console_attr.Decode(arg) for arg in sys.argv]


def _FullPath(tool_dir, exec_name):
  return os.path.join(SDK_ROOT, tool_dir, exec_name)


def ExecutePythonTool(tool_dir, exec_name, *args):
  """Execute the given python script with the given args and command line.

  Args:
    tool_dir: the directory the tool is located in
    exec_name: additional path to the executable under the tool_dir
    *args: args for the command
  """
  py_path = None  # Let execution_utils resolve the path.
  # Gsutil allows users to set the desired Python interpreter using a separate
  # environment variable, so as to allow users to run gsutil using Python 3
  # without forcing the rest of the Cloud SDK to use Python 3 (as it would
  # likely break at the time this comment was written).
  extra_popen_kwargs = {}
  if exec_name == 'gsutil':
    gsutil_py = encoding.GetEncodedValue(os.environ, 'CLOUDSDK_GSUTIL_PYTHON')
    # Since PY3, Python closes open FDs in child processes, since we need them
    # open for completions to work we set the close_fds kwarg to Popen.
    extra_popen_kwargs['close_fds'] = False
    if gsutil_py:
      py_path = gsutil_py

  if exec_name == 'bq.py':
    bq_py = encoding.GetEncodedValue(os.environ, 'CLOUDSDK_BQ_PYTHON')
    if bq_py:
      py_path = bq_py

  if exec_name == 'dev_appserver.py':
    if platforms.OperatingSystem.Current() != platforms.OperatingSystem.WINDOWS:
      # Hard code the python to 'python2' which should exist on most Unix based
      # systems.
      py_path = 'python2'
    else:
      # On windows attempt to use the bundled PY2 component.
      bundled_py2 = os.path.join(
          SDK_ROOT, 'platform', 'bundledpython2', 'python.exe')
      if os.path.exists(bundled_py2):
        py_path = bundled_py2

    # Set this ENV var to explicitly select the python binary to use for
    # dev_appserver.py
    devapp_env_py = encoding.GetEncodedValue(os.environ,
                                             'CLOUDSDK_DEVAPPSERVER_PYTHON')
    if devapp_env_py:
      py_path = devapp_env_py

  _ExecuteTool(
      execution_utils.ArgsForPythonTool(
          _FullPath(tool_dir, exec_name), *args, python=py_path),
      **extra_popen_kwargs)


def ExecuteJarTool(java_bin, jar_dir, jar_name, classname, flags=None, *args):
  """Execute a given jar with the given args and command line.

  Args:
    java_bin: str, path to the system Java binary
    jar_dir: str, the directory the jar is located in
    jar_name: str, file name of the jar under tool_dir
    classname: str, name of the main class in the jar
    flags: [str], flags for the java binary
    *args: args for the command
  """
  flags = flags or []
  jar_path = _FullPath(jar_dir, jar_name)
  classname_arg = [classname] if classname else []
  java_args = ['-cp', jar_path] + flags + classname_arg + list(args)
  _ExecuteTool(
      execution_utils.ArgsForExecutableTool(java_bin, *java_args))


def ExecuteJavaClass(java_bin,
                     jar_dir,
                     main_jar,
                     main_class,
                     java_flags=None,
                     main_args=None):
  """Execute a given java class within a directory of jars.

  Args:
    java_bin: str, path to the system Java binary
    jar_dir: str, directory of jars to put on class path
    main_jar: str, main jar (placed first on class path)
    main_class: str, name of the main class in the jar
    java_flags: [str], flags for the java binary
    main_args: args for the command
  """
  java_flags = java_flags or []
  main_args = main_args or []
  jar_dir_path = os.path.join(SDK_ROOT, jar_dir, '*')
  main_jar_path = os.path.join(SDK_ROOT, jar_dir, main_jar)
  classpath = main_jar_path + os.pathsep + jar_dir_path
  java_args = (['-cp', classpath]
               + list(java_flags)
               + [main_class]
               + list(main_args))
  _ExecuteTool(execution_utils.ArgsForExecutableTool(java_bin, *java_args))


def ExecuteShellTool(tool_dir, exec_name, *args):
  """Execute the given bash script with the given args.

  Args:
    tool_dir: the directory the tool is located in
    exec_name: additional path to the executable under the tool_dir
    *args: args for the command
  """
  _ExecuteTool(
      execution_utils.ArgsForExecutableTool(_FullPath(tool_dir, exec_name),
                                            *args))


def ExecuteCMDTool(tool_dir, exec_name, *args):
  """Execute the given batch file with the given args.

  Args:
    tool_dir: the directory the tool is located in
    exec_name: additional path to the executable under the tool_dir
    *args: args for the command
  """
  _ExecuteTool(
      execution_utils.ArgsForCMDTool(_FullPath(tool_dir, exec_name), *args))


def _GetToolEnv():
  env = dict(os.environ)
  encoding.SetEncodedValue(env, 'CLOUDSDK_WRAPPER', '1')
  encoding.SetEncodedValue(env, 'CLOUDSDK_VERSION', config.CLOUD_SDK_VERSION)
  encoding.SetEncodedValue(env, 'CLOUDSDK_PYTHON',
                           execution_utils.GetPythonExecutable())
  return env


def _ExecuteTool(args, **extra_popen_kwargs):
  """Executes a new tool with the given args, plus the args from the cmdline.

  Args:
    args: [str], The args of the command to execute.
    **extra_popen_kwargs: [dict], kwargs to be unpacked in Popen call for tool.
  """
  execution_utils.Exec(
      args + sys.argv[1:], env=_GetToolEnv(), **extra_popen_kwargs)


def GetDefaultInstalledComponents():
  """Gets the list of components to install by default.

  Returns:
    list(str), The component ids that should be installed.  It will return []
    if there are no default components, or if there is any error in reading
    the file with the defaults.
  """
  default_components_file = os.path.join(BOOTSTRAPPING_DIR,
                                         '.default_components')
  try:
    with open(default_components_file) as f:
      return json.load(f)
  # pylint:disable=bare-except, If the file does not exist or is malformed,
  # we don't want to expose this as an error.  Setup will just continue
  # without installing any components by default and will tell the user how
  # to install the components they want manually.
  except:
    pass
  return []


def WarnAndExitOnBlockedCommand(args, blocked_commands):
  """Block certain subcommands, warn the user, and exit.

  Args:
    args: the command line arguments, including the 0th argument which is
      the program name.
    blocked_commands: a map of blocked commands to the messages that should be
      printed when they're run.
  """
  bad_arg = None
  for arg in args[1:]:
    # Flags are skipped and --flag=value are skipped. It is possible for
    # '--flag value' to result in a false positive if value happens to be a
    # blocked command.
    if arg and arg[0] == '-':
      continue
    if arg in blocked_commands:
      bad_arg = arg
      break

  blocked = bad_arg is not None

  if blocked:
    sys.stderr.write('It looks like you are trying to run "%s %s".\n'
                     % (args[0], bad_arg))
    sys.stderr.write('The "%s" command is no longer needed with the '
                     'Cloud SDK.\n' % bad_arg)
    sys.stderr.write(blocked_commands[bad_arg] + '\n')
    answer = input('Really run this command? (y/N) ')
    if answer not in ['y', 'Y']:
      sys.exit(1)


def CheckUpdates(command_path):
  """Check for updates and inform the user.

  Args:
    command_path: str, The '.' separated path of the command that is currently
      being run (i.e. gcloud.foo.bar).
  """
  try:
    update_manager.UpdateManager.PerformUpdateCheck(command_path=command_path)
  # pylint:disable=broad-except, We never want this to escape, ever. Only
  # messages printed should reach the user.
  except Exception:
    pass


def CommandStart(command_name, component_id=None, version=None):
  """Logs that the given command is being executed.

  Args:
    command_name: str, The name of the command being executed.
    component_id: str, The component id that this command belongs to.  Used for
      version information if version was not specified.
    version: str, Directly use this version instead of deriving it from
      component.
  """
  if version is None and component_id:
    version = local_state.InstallationState.VersionForInstalledComponent(
        component_id)
  metrics.Executions(command_name, version)


def GetActiveProjectAndAccount():
  """Get the active project name and account for the active credentials.

  For use with wrapping legacy tools that take projects and credentials on
  the command line.

  Returns:
    (str, str), A tuple whose first element is the project, and whose second
    element is the account.
  """
  project_name = properties.VALUES.core.project.Get(validate=False)
  account = properties.VALUES.core.account.Get(validate=False)
  return (project_name, account)


def ReadFileContents(*path_parts):
  """Returns file content at specified relative path wrt SDK root path."""
  return files.ReadFileContents(os.path.join(SDK_ROOT, *path_parts)).strip()


# Register some other sources for credentials and project.
c_store.GceCredentialProvider().Register()
