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

"""Package containing fingerprinting for all runtimes.
"""


from gae_ext_runtime import ext_runtime

from googlecloudsdk.api_lib.app import ext_runtime_adapter
from googlecloudsdk.api_lib.app.runtimes import python
from googlecloudsdk.api_lib.app.runtimes import python_compat
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log

RUNTIMES = [
    # Note that ordering of runtimes here is very important and changes to the
    # relative positions need to be tested carefully.

    # Custom comes first, if we've got a Dockerfile this is a custom runtime.
    ext_runtime_adapter.CoreRuntimeLoader('custom', 'Custom',
                                          ['custom']),

    # Go's position is relatively flexible due to its orthogonal nature.
    ext_runtime_adapter.CoreRuntimeLoader('go', 'Go', ['go', 'custom']),

    ext_runtime_adapter.CoreRuntimeLoader('ruby', 'Ruby', ['ruby', 'custom']),
    ext_runtime_adapter.CoreRuntimeLoader('nodejs', 'Node.js',
                                          ['nodejs', 'custom']),
    ext_runtime_adapter.CoreRuntimeLoader('java', 'Java',
                                          ['java', 'java7', 'custom']),
    python_compat,

    # Python and PHP are last because they match if any .py or .php file is
    # present.
    ext_runtime_adapter.CoreRuntimeLoader('python', 'Python',
                                          ['python', 'custom']),
    ext_runtime_adapter.CoreRuntimeLoader('php', 'PHP', ['php', 'custom']),
]


class UnidentifiedDirectoryError(exceptions.Error):
  """Raised when GenerateConfigs() can't identify the directory."""

  def __init__(self, path):
    """Constructor.

    Args:
      path: (basestring) Directory we failed to identify.
    """
    super(UnidentifiedDirectoryError, self).__init__(
        'Unrecognized directory type: [{0}]'.format(path))
    self.path = path


class ExtRuntimeError(exceptions.Error):
  """ext_runtime.Error errors are converted to this."""


class ConflictingConfigError(exceptions.Error):
  """Property in app.yaml conflicts with params passed to fingerprinter."""


class AlterConfigFileError(exceptions.Error):
  """Error when attempting to update an existing config file (app.yaml)."""

  def __init__(self, inner_exception):
    super(AlterConfigFileError, self).__init__(
        'Could not alter app.yaml due to an internal error:\n{0}\n'
        'Please update app.yaml manually.'.format(inner_exception))


def IdentifyDirectory(path, params=None):
  """Try to identify the given directory.

  As a side-effect, if there is a config file in 'params' with a runtime of
  'custom', this sets params.custom to True.

  Args:
    path: (basestring) Root directory to identify.
    params: (ext_runtime.Params or None) Parameters passed through to the
      fingerprinters.  Uses defaults if not provided.

  Returns:
    (ext_runtime.Configurator or None) Returns a module if we've identified
    it, None if not.
  """
  if not params:
    params = ext_runtime.Params()

  # Parameter runtime has precedence
  if params.runtime:
    specified_runtime = params.runtime
  elif params.appinfo:
    specified_runtime = params.appinfo.GetEffectiveRuntime()
  else:
    specified_runtime = None

  if specified_runtime == 'custom':
    params.custom = True

  for runtime in RUNTIMES:

    # If we have an app.yaml, don't fingerprint for any runtimes that don't
    # allow the runtime name it specifies.
    if (specified_runtime and runtime.ALLOWED_RUNTIME_NAMES and
        specified_runtime not in runtime.ALLOWED_RUNTIME_NAMES):
      log.info('Not checking for [%s] because runtime is [%s]' %
               (runtime.NAME, specified_runtime))
      continue

    try:
      configurator = runtime.Fingerprint(path, params)
    except ext_runtime.Error as ex:
      raise ExtRuntimeError(ex.message)
    if configurator:
      return configurator
  return None


def _GetModule(path, params=None, config_filename=None):
  """Helper function for generating configs.

  Args:
    path: (basestring) Root directory to identify.
    params: (ext_runtime.Params or None) Parameters passed through to the
      fingerprinters.  Uses defaults if not provided.
    config_filename: (str or None) Filename of the config file (app.yaml).

  Raises:
    UnidentifiedDirectoryError: No runtime module matched the directory.
    ConflictingConfigError: Current app.yaml conflicts with other params.

  Returns:
    ext_runtime.Configurator, the configurator for the path
  """
  if not params:
    params = ext_runtime.Params()

  config = params.appinfo
  # An app.yaml exists, results in a lot more cases
  if config and not params.deploy:
    # Enforce --custom
    if not params.custom:
      raise ConflictingConfigError(
          'Configuration file already exists. This command generates an '
          'app.yaml configured to run an application on Google App Engine. '
          'To create the configuration files needed to run this '
          'application with docker, try `gcloud preview app gen-config '
          '--custom`.')
    # Check that current config is for MVM
    if not config.IsVm():
      raise ConflictingConfigError(
          'gen-config is only supported for App Engine Flexible.  Please '
          'use "vm: true" in your app.yaml if you would like to use App Engine '
          'Flexible to run your application.')
    # Check for conflicting --runtime and runtime in app.yaml
    if (config.GetEffectiveRuntime() != 'custom' and params.runtime is not None
        and params.runtime != config.GetEffectiveRuntime()):
      raise ConflictingConfigError(
          '[{0}] contains "runtime: {1}" which conficts with '
          '--runtime={2}.'.format(config_filename, config.GetEffectiveRuntime(),
                                  params.runtime))

  module = IdentifyDirectory(path, params)
  if not module:
    raise UnidentifiedDirectoryError(path)
  return module


def GenerateConfigs(path, params=None, config_filename=None):
  """Identify runtime and generate config files for a directory.

  If a runtime can be identified for the given directory, calls the runtime's
  GenerateConfigs method, which writes configs to the directory.

  Args:
    path: (basestring) Root directory to identify.
    params: (ext_runtime.Params or None) Parameters passed through to the
      fingerprinters.  Uses defaults if not provided.
    config_filename: (str or None) Filename of the config file (app.yaml).

  Raises:
    ExtRuntimeError: if there was an error generating configs

  Returns:
    (bool): True if files were written
  """
  module = _GetModule(path, params=params, config_filename=config_filename)

  try:
    return module.GenerateConfigs()
  except ext_runtime.Error as ex:
    raise ExtRuntimeError(ex.message)


def GenerateConfigData(path, params=None, config_filename=None):
  """Identify runtime and generate contents of config files for a directory.

  If a runtime can be identified for the given directory, calls the runtime's
  GenerateConfigData method, which generates the contents of config files.

  Args:
    path: (basestring) Root directory to identify.
    params: (ext_runtime.Params or None) Parameters passed through to the
      fingerprinters.  Uses defaults if not provided.
    config_filename: (str or None) Filename of the config file (app.yaml).

  Raises:
    ExtRuntimeError: if there was an error generating configs

  Returns:
    [ext_runtime.GeneratedFile] generated config files.
  """
  module = _GetModule(path, params=params, config_filename=config_filename)

  try:
    return module.GenerateConfigData()
  except ext_runtime.Error as ex:
    raise ExtRuntimeError(ex.message)
