# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Flags for serverless local development setup."""

from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.args import map_util
from googlecloudsdk.core import exceptions
import six


class FlagDef(object):
  """Object that holds a flag definition and adds it to a parser."""

  def __init__(self, name, **kwargs):
    self.name = name
    self.kwargs = kwargs

  def __eq__(self, other):
    return self.name == other.name

  def __ne__(self, other):
    return self.name != other.name

  def __hash__(self):
    return hash(self.name)

  def ConfigureParser(self, parser):
    parser.add_argument(self.name, **self.kwargs)


class FlagDefs(object):
  """Base type for all flag builders."""

  def __init__(self):
    self._operations = set()

  def _AddFlag(self, name, **kwargs):
    self._AddOperation(FlagDef(name, **kwargs))

  def _AddOperation(self, operation):
    self._operations.add(operation)

  def ConfigureParser(self, parser):
    for operation in self._operations:
      operation.ConfigureParser(parser)


class MutuallyExclusiveGroupDef(FlagDefs):
  """Flag builder where all flags are added to a mutually exclusive group."""

  def ConfigureParser(self, parser):
    group = parser.add_mutually_exclusive_group(required=False)
    for op in self._operations:
      op.ConfigureParser(group)


class BuilderFlags(MutuallyExclusiveGroupDef):
  """Flags for builder settings."""

  def AddDockerfile(self):
    self._AddFlag(
        '--dockerfile',
        default='Dockerfile',
        help='Dockerfile for the service image.')

  def AddBuilder(self):
    self._AddFlag(
        '--builder',
        help='Build with a given Cloud Native Computing Foundation Buildpack '
        'builder.')


class CredentialFlags(MutuallyExclusiveGroupDef):

  def AddServiceAccount(self):
    self._AddFlag(
        '--service-account',
        help='When connecting to Google Cloud Platform services, use a service '
        'account key.')

  def AddApplicationDefaultCredential(self):
    self._AddFlag(
        '--application-default-credential',
        action='store_true',
        default=False,
        help='When connecting to Google Cloud Platform services, use the '
        'application default credential.')


class EnvVarFlags(MutuallyExclusiveGroupDef):
  """Environment variable flags."""

  def AddEnvVars(self):
    self._AddFlag(
        '--env-vars',
        metavar='KEY=VALUE',
        action=arg_parsers.UpdateAction,
        type=arg_parsers.ArgDict(
            key_type=six.text_type, value_type=six.text_type),
        help='List of key-value pairs to set as environment variables.')

  def AddEnvVarsFile(self):
    self._AddFlag(
        '--env-vars-file',
        metavar='FILE_PATH',
        type=map_util.ArgDictFile(
            key_type=six.text_type, value_type=six.text_type),
        help='Path to a local YAML file with definitions for all environment '
        'variables.')


class CommonFlags(FlagDefs):
  """Flags that are common between the gcloud code dev commands."""

  def __init__(self):
    super(CommonFlags, self).__init__()
    self._group_cache = {}

  def AddLocalPort(self):
    self._AddFlag(
        '--local-port',
        type=int,
        help='Local port to which the service connection is forwarded. If this '
        'flag is not set, then a random port is chosen.')

  def AddSource(self):
    self._AddFlag(
        '--source',
        help='The directory containing the source to build. '
        'If not specified, the current directory is used.')

  def AddServiceName(self):
    self._AddFlag('--service-name', required=False, help='Name of the service.')

  def AddImage(self):
    self._AddFlag('--image', required=False, help='Name for the built image.')

  def AddMemory(self):
    self._AddFlag(
        '--memory',
        type=arg_parsers.BinarySize(default_unit='B'),
        help='Container memory limit. Limit is expressed either as an integer '
        'representing the number of bytes or an integer followed by a unit '
        'suffix. Valid unit suffixes are "B", "KB", "MB", "GB", "TB", "KiB", '
        '"MiB", "GiB", "TiB", or "PiB".')

  def AddCpu(self):
    self._AddFlag(
        '--cpu',
        type=arg_parsers.BoundedFloat(lower_bound=0.0),
        help='Container CPU limit. Limit is expressed as a number of CPUs. '
        'Fractional CPU limits are allowed (e.g. 1.5).')

  def AddCloudsqlInstances(self):
    self._AddFlag(
        '--cloudsql-instances',
        type=arg_parsers.ArgList(),
        metavar='CLOUDSQL_INSTANCE',
        help='Cloud SQL instance connection strings. Must be in the form '
        '<project>:<region>:<instance>.')

  def AddReadinessProbe(self):
    # This flag launches the readiness probe feature. It is currently
    # default off. It will be moved to default on when ready and then
    # the feature will be always on.
    self._AddFlag(
        '--readiness-probe',
        default=False,
        action='store_true',
        hidden=True,
        help='Add a readiness probe to the list of containers that delays '
        'deployment stabilization until the application app has bound to $PORT')

  def AddServiceConfigPositionalArg(self, include_app_engine_docs=False):
    """_AddFlag for service_config, which has two possible help strings.

    Args:
      include_app_engine_docs: Add paragraph that says app.yaml is allowed.
    """
    help_text = (
        'service.yaml filename override. Defaults to the first file '
        'matching ```*service.dev.yaml``` then ```*service.yaml```, if any '
        'exist. This path is relative to the --source dir.')
    if include_app_engine_docs:
      help_text += (
          '\n'
          'An App Engine config path (typically ```app.yaml```) may also be '
          'provided here, and we will build with a Cloud Native Computing '
          'Foundation Buildpack builder selected from '
          'gcr.io/gae-runtimes/buildpacks, according to the App Engine '
          '```runtime``` specified in app.yaml.')

    self._AddFlag(
        'service_config',
        metavar='SERVICE_CONFIG',
        nargs='?',
        help=help_text,
    )

  def AddAllowSecretManagerFlag(self):
    self._AddFlag(
        '--allow-secret-manager',
        action=arg_parsers.StoreTrueFalseAction,
        help=('Suppress warnings if secrets need to be pulled from secret '
              'manager'))

  def AddSecrets(self):
    self._AddFlag(
        '--secrets',
        metavar='KEY=VALUE',
        action=arg_parsers.UpdateAction,
        type=arg_parsers.ArgDict(
            key_type=six.text_type, value_type=six.text_type),
        help='List of key-value pairs to set as secrets.')

  def AddCloud(self):
    self._AddFlag(
        '--cloud',
        default=False,
        action='store_true',
        hidden=True,
        help='deploy code to Cloud Run')
    self._AddFlag(
        '--region', help='region to deploy the dev service', hidden=True)

  def _GetGroup(self, klass):
    if klass not in self._group_cache:
      group = klass()
      self._group_cache[klass] = group
      self._AddOperation(group)

    return self._group_cache[klass]

  def CredentialsGroup(self):
    return self._GetGroup(CredentialFlags)

  def EnvVarsGroup(self):
    return self._GetGroup(EnvVarFlags)

  def BuildersGroup(self):
    return self._GetGroup(BuilderFlags)

  def AddAlphaAndBetaFlags(self, release_track):
    self._AddBetaFlags()

    if release_track == base.ReleaseTrack.ALPHA:
      self._AddAlphaFlags()
    # See AssembleSettings for where we decide how to parse service_config args
    # based on release track.
    appyaml_support = release_track == base.ReleaseTrack.ALPHA
    self.AddServiceConfigPositionalArg(include_app_engine_docs=appyaml_support)

  def _AddBetaFlags(self):
    """Set up flags that are for alpha and beta tracks."""
    self.BuildersGroup().AddDockerfile()
    self.AddSource()
    self.AddLocalPort()
    self.CredentialsGroup().AddServiceAccount()
    self.CredentialsGroup().AddApplicationDefaultCredential()
    self.AddReadinessProbe()
    self.AddAllowSecretManagerFlag()
    self.AddSecrets()
    self.BuildersGroup().AddBuilder()

  def _AddAlphaFlags(self):
    """Set up flags that are for alpha track only."""

    self.AddCloudsqlInstances()
    self.AddServiceName()
    self.AddImage()
    self.AddMemory()
    self.AddCpu()
    self.EnvVarsGroup().AddEnvVars()
    self.EnvVarsGroup().AddEnvVarsFile()
    self.AddCloud()


class InvalidFlagError(exceptions.Error):
  """Flag settings are illegal."""


def Validate(namespace):
  """Validate flag requirements that cannot be handled by argparse."""
  if ('cloudsql_instances' in namespace and
      namespace.IsSpecified('cloudsql_instances') and
      not (namespace.IsSpecified('service_account') or
           namespace.IsSpecified('application_default_credential'))):
    raise InvalidFlagError('--cloudsql-instances requires --service-account or '
                           '--application-default-credential to be specified.')
