# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Shared resource flags for Cloud Run commands."""


import abc
import copy
import os
import re

from googlecloudsdk.api_lib.run import global_methods
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.calliope.concepts import util as concepts_util
from googlecloudsdk.command_lib.run import exceptions
from googlecloudsdk.command_lib.run import platforms
from googlecloudsdk.command_lib.util.concepts import presentation_specs
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
from googlecloudsdk.core.console import console_io


class PromptFallthrough(deps.Fallthrough):
  """Fall through to reading from an interactive prompt."""

  def __init__(self, hint):
    super(PromptFallthrough, self).__init__(function=None, hint=hint)

  @abc.abstractmethod
  def _Prompt(self, parsed_args):
    pass

  def _Call(self, parsed_args):
    if not console_io.CanPrompt():
      return None
    return self._Prompt(parsed_args)


def _GenerateServiceName(image):
  """Produce a valid default service name from a container image path.

  Converts a file path or image path into a reasonable default service name by
  stripping file path delimeters, image tags, and image hashes.
  For example, the image name 'gcr.io/myproject/myimage:latest' would produce
  the service name 'myimage'.

  Args:
    image: str, The container path.

  Returns:
    A valid Cloud Run service name.
  """
  base_name = os.path.basename(image.rstrip(os.sep))
  base_name = base_name.split(':')[0]  # Discard image tag if present.
  base_name = base_name.split('@')[0]  # Disacard image hash if present.
  # Remove non-supported special characters.
  return re.sub(r'[^a-zA-Z0-9-]', '', base_name).strip('-').lower()


def _GenerateServiceNameFromLocalPath(source):
  """Produce a valid default service name from a local file or directory path.

  Converts a file or directory path into a reasonable default service name by
  resolving relative paths to absolute paths, removing any extensions, and then
  removing any invalid characters.

  For example, the paths /tmp/foo/bar/.. and /tmp/foo.tar.gz would both produce
  the service name 'foo'. A source path of "." will be expanded to the current
  directory name."

  Args:
    source: str, The file or directory path.

  Returns:
    A valid Cloud Run service name.
  """
  path, ext = os.path.splitext(os.path.abspath(source))
  while ext:
    path, ext = os.path.splitext(path)
  return _GenerateServiceName(path)


class ResourcePromptFallthrough(PromptFallthrough):
  """Fall through to reading the resource name from an interactive prompt."""

  def __init__(self, resource_type_lower):
    super(ResourcePromptFallthrough, self).__init__(
        'specify the {} name from an interactive prompt'.format(
            resource_type_lower
        )
    )
    self.resource_type_lower = resource_type_lower

  def _Prompt(self, parsed_args):
    message = self.resource_type_lower.capitalize() + ' name'
    default_name = self._DefaultNameFromArgs(parsed_args)
    return console_io.PromptWithDefault(message=message, default=default_name)

  def _DefaultNameFromArgs(self, parsed_args):
    if getattr(parsed_args, 'image', None):
      return _GenerateServiceName(parsed_args.image)
    elif getattr(parsed_args, 'source', None):
      return _GenerateServiceNameFromLocalPath(parsed_args.source)
    return ''


class RegionPromptFallthrough(PromptFallthrough):
  """Fall through to reading the region from an interactive prompt."""

  def __init__(self):
    super(RegionPromptFallthrough, self).__init__(
        'specify the region from an interactive prompt'
    )

  def _Prompt(self, parsed_args):
    client = global_methods.GetServerlessClientInstance()
    all_regions = global_methods.ListRegions(client)
    idx = console_io.PromptChoice(
        all_regions,
        message='Please specify a region:\n',
        cancel_option=True,
        allow_freeform=True,
    )
    region = all_regions[idx]
    log.status.Print(
        'To make this the default region, run '
        '`gcloud config set run/region {}`.\n'.format(region)
    )
    if region:
      parsed_args.region = region
    return region


class ServicePromptFallthrough(ResourcePromptFallthrough):

  def __init__(self):
    super(ServicePromptFallthrough, self).__init__('service')


class WorkerPoolPromptFallthrough(ResourcePromptFallthrough):

  def __init__(self):
    super(WorkerPoolPromptFallthrough, self).__init__('workerpool')


class JobPromptFallthrough(ResourcePromptFallthrough):

  def __init__(self):
    super(JobPromptFallthrough, self).__init__('job')


class InstancePromptFallthrough(ResourcePromptFallthrough):

  def __init__(self):
    super(InstancePromptFallthrough, self).__init__('instance')


class ExecutionPromptFallthrough(ResourcePromptFallthrough):

  def __init__(self):
    super(ExecutionPromptFallthrough, self).__init__('execution')


class DefaultFallthrough(deps.Fallthrough):
  """Use the namespace "default".

  For Knative only.

  For Cloud Run, raises an ArgumentError if project not set.
  """

  def __init__(self):
    super(DefaultFallthrough, self).__init__(
        function=None,
        hint=(
            'For Cloud Run on Kubernetes Engine, defaults to "default". '
            'Otherwise, defaults to project ID.'
        ),
    )

  def _Call(self, parsed_args):
    if (
        platforms.GetPlatform() == platforms.PLATFORM_GKE
        or platforms.GetPlatform() == platforms.PLATFORM_KUBERNETES
    ):
      return 'default'
    elif not (
        getattr(parsed_args, 'project', None)
        or properties.VALUES.core.project.Get()
    ):
      # HACK: Compensate for how "namespace" is actually "project" in Cloud Run
      # by providing an error message explicitly early here.
      raise exceptions.ArgumentError(
          'The [project] resource is not properly specified. '
          'Please specify the argument [--project] on the command line or '
          'set the property [core/project].'
      )
    return None


def NamespaceAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='namespace',
      help_text=(
          'Specific to Cloud Run for Anthos: '
          'Kubernetes namespace for the {resource}.'
      ),
      fallthroughs=[
          deps.PropertyFallthrough(properties.VALUES.run.namespace),
          DefaultFallthrough(),
          deps.ArgFallthrough('project'),
          deps.PropertyFallthrough(properties.VALUES.core.project),
      ],
  )


def ProjectAttributeConfig():
  project_attribute_config = copy.deepcopy(
      concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG
  )
  fallthroughs = [
      DefaultFallthrough()
  ] + concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG.fallthroughs
  project_attribute_config.fallthroughs = fallthroughs
  return project_attribute_config


def ServiceAttributeConfig(prompt=False):
  """Attribute config with fallthrough prompt only if requested."""
  if prompt:
    fallthroughs = [ServicePromptFallthrough()]
  else:
    fallthroughs = []
  return concepts.ResourceParameterAttributeConfig(
      name='service',
      help_text='Service for the {resource}.',
      fallthroughs=fallthroughs,
  )


def WorkerPoolAttributeConfig(prompt=False):
  """Attribute config with fallthrough prompt only if requested."""
  if prompt:
    fallthroughs = [WorkerPoolPromptFallthrough()]
  else:
    fallthroughs = []
  return concepts.ResourceParameterAttributeConfig(
      name='worker-pool',
      help_text='WorkerPool for the {resource}.',
      fallthroughs=fallthroughs,
  )


def ConfigurationAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='configuration', help_text='Configuration for the {resource}.'
  )


def RouteAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='route', help_text='Route for the {resource}.'
  )


def RevisionAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='revision', help_text='Revision for the {resource}.'
  )


def DomainAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='domain', help_text='Name of the domain to be mapped to.'
  )


def JobAttributeConfig(prompt=False):
  if prompt:
    fallthroughs = [JobPromptFallthrough()]
  else:
    fallthroughs = []
  return concepts.ResourceParameterAttributeConfig(
      name='jobs',
      help_text='Job for the {resource}.',
      fallthroughs=fallthroughs,
  )


def InstanceAttributeConfig(prompt=False):
  if prompt:
    fallthroughs = [InstancePromptFallthrough()]
  else:
    fallthroughs = []
  return concepts.ResourceParameterAttributeConfig(
      name='instances',
      help_text='Instance for the {resource}.',
      fallthroughs=fallthroughs,
  )


def ExecutionAttributeConfig(prompt=False):
  if prompt:
    fallthroughs = [ExecutionPromptFallthrough()]
  else:
    fallthroughs = []
  return concepts.ResourceParameterAttributeConfig(
      name='executions', help_text='Execution.', fallthroughs=fallthroughs
  )


class TaskExecutionAndIndexFallthrough(deps.ArgFallthrough):
  """Allow the user to provide --execution and --index to find a task."""

  def __init__(self, arg_name, plural=False):
    super(TaskExecutionAndIndexFallthrough, self).__init__(
        'provide the arguments `{}`  and `index` on the command line'.format(
            arg_name
        ),
        active=True,
        plural=plural,
    )
    self.arg_name = arg_name

  def _Call(self, parsed_args):
    prefix = getattr(
        parsed_args, concepts_util.NamespaceFormat(self.arg_name), None
    )
    index = getattr(parsed_args, 'index', None)
    return '{}-{}'.format(prefix, index)


def TaskAttributeConfig(prompt=False):
  if prompt:
    fallthroughs = [TaskExecutionAndIndexFallthrough('task')]
  else:
    fallthroughs = []
  return concepts.ResourceParameterAttributeConfig(
      name='tasks', help_text='Task.', fallthroughs=fallthroughs
  )


def LocationAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='region',
      help_text=(
          'The Cloud region for the {resource}. Overrides the default '
          '`run/region` property value for this command invocation.'
      ),
      fallthroughs=[
          deps.ArgFallthrough('--region'),
          deps.PropertyFallthrough(properties.VALUES.run.region),
          RegionPromptFallthrough(),
      ],
  )


class ClusterPromptFallthrough(PromptFallthrough):
  """Fall through to reading the cluster name from an interactive prompt."""

  def __init__(self):
    super(ClusterPromptFallthrough, self).__init__(
        'specify the cluster from a list of available clusters'
    )

  def _Prompt(self, parsed_args):
    """Fallthrough to reading the cluster name from an interactive prompt.

    Only prompt for cluster name if the user-specified platform is GKE.

    Args:
      parsed_args: Namespace, the args namespace.

    Returns:
      A cluster name string
    """
    if platforms.GetPlatform() != platforms.PLATFORM_GKE:
      return

    project = properties.VALUES.core.project.Get(required=True)
    cluster_location = (
        getattr(parsed_args, 'cluster_location', None)
        or properties.VALUES.run.cluster_location.Get()
    )
    cluster_location_msg = (
        ' in [{}]'.format(cluster_location) if cluster_location else ''
    )

    cluster_refs = global_methods.MultiTenantClustersForProject(
        project, cluster_location
    )
    if not cluster_refs:
      raise exceptions.ConfigurationError(
          'No compatible clusters found{}. '
          'Ensure your cluster has Cloud Run enabled.'.format(
              cluster_location_msg
          )
      )

    cluster_refs_descs = [
        self._GetClusterDescription(c, cluster_location, project)
        for c in cluster_refs
    ]

    idx = console_io.PromptChoice(
        cluster_refs_descs,
        message='GKE cluster{}:'.format(cluster_location_msg),
        cancel_option=True,
    )

    cluster_ref = cluster_refs[idx]

    if cluster_location:
      location_help_text = ''
    else:
      location_help_text = (
          ' && gcloud config set run/cluster_location {}'.format(
              cluster_ref.zone
          )
      )

    cluster_name = cluster_ref.Name()

    if cluster_ref.projectId != project:
      cluster_name = cluster_ref.RelativeName()
      location_help_text = ''

    log.status.Print(
        'To make this the default cluster, run '
        '`gcloud config set run/cluster {cluster}'
        '{location}`.\n'.format(
            cluster=cluster_name, location=location_help_text
        )
    )
    return cluster_ref.SelfLink()

  def _GetClusterDescription(self, cluster, cluster_location, project):
    """Description of cluster for prompt."""

    response = cluster.Name()
    if not cluster_location:
      response = '{} in {}'.format(response, cluster.zone)
    if project != cluster.projectId:
      response = '{} in {}'.format(response, cluster.projectId)

    return response


def ClusterAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='cluster',
      help_text=(
          'Name of the Kubernetes Engine cluster to use. '
          'Alternatively, set the property [run/cluster].'
      ),
      fallthroughs=[
          deps.PropertyFallthrough(properties.VALUES.run.cluster),
          ClusterPromptFallthrough(),
      ],
  )


class ClusterLocationPromptFallthrough(PromptFallthrough):
  """Fall through to reading the cluster name from an interactive prompt."""

  def __init__(self):
    super(ClusterLocationPromptFallthrough, self).__init__(
        'specify the cluster location from a list of available zones'
    )

  def _Prompt(self, parsed_args):
    """Fallthrough to reading the cluster location from an interactive prompt.

    Only prompt for cluster location if the user-specified platform is GKE
    and if cluster name is already defined.

    Args:
      parsed_args: Namespace, the args namespace.

    Returns:
      A cluster location string
    """
    cluster_name = (
        getattr(parsed_args, 'cluster', None)
        or properties.VALUES.run.cluster.Get()
    )
    if platforms.GetPlatform() == platforms.PLATFORM_GKE and cluster_name:
      clusters = [
          c for c in global_methods.ListClusters() if c.name == cluster_name
      ]
      if not clusters:
        raise exceptions.ConfigurationError(
            'No cluster locations found for cluster [{}]. '
            'Ensure your clusters have Cloud Run enabled.'.format(cluster_name)
        )
      cluster_locations = [c.zone for c in clusters]
      idx = console_io.PromptChoice(
          cluster_locations,
          message='GKE cluster location for [{}]:'.format(cluster_name),
          cancel_option=True,
      )
      location = cluster_locations[idx]
      log.status.Print(
          'To make this the default cluster location, run '
          '`gcloud config set run/cluster_location {}`.\n'.format(location)
      )
      return location


def ClusterLocationAttributeConfig():
  return concepts.ResourceParameterAttributeConfig(
      name='location',
      help_text=(
          'Zone in which the {resource} is located. '
          'Alternatively, set the property [run/cluster_location].'
      ),
      fallthroughs=[
          deps.PropertyFallthrough(properties.VALUES.run.cluster_location),
          ClusterLocationPromptFallthrough(),
      ],
  )


def GetClusterResourceSpec():
  return concepts.ResourceSpec(
      'container.projects.zones.clusters',
      projectId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      zone=ClusterLocationAttributeConfig(),
      clusterId=ClusterAttributeConfig(),
      resource_name='cluster',
  )


def GetServiceResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.namespaces.services',
      namespacesId=NamespaceAttributeConfig(),
      servicesId=ServiceAttributeConfig(prompt),
      resource_name='service',
  )


def GetConfigurationResourceSpec():
  return concepts.ResourceSpec(
      'run.namespaces.configurations',
      namespacesId=NamespaceAttributeConfig(),
      configurationsId=ConfigurationAttributeConfig(),
      resource_name='configuration',
  )


def GetRouteResourceSpec():
  return concepts.ResourceSpec(
      'run.namespaces.routes',
      namespacesId=NamespaceAttributeConfig(),
      routesId=RouteAttributeConfig(),
      resource_name='route',
  )


# For Worker Pool revisions, we don't use the namespace attribute.
def GetRevisionResourceSpec(is_worker_pool_revision=False):
  return concepts.ResourceSpec(
      'run.namespaces.revisions',
      namespacesId=NamespaceAttributeConfig()
      if not is_worker_pool_revision
      else concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      revisionsId=RevisionAttributeConfig(),
      resource_name='revision',
  )


def GetDomainMappingResourceSpec():
  return concepts.ResourceSpec(
      'run.namespaces.domainmappings',
      namespacesId=NamespaceAttributeConfig(),
      domainmappingsId=DomainAttributeConfig(),
      resource_name='DomainMapping',
  )


def GetJobResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.namespaces.jobs',
      namespacesId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      jobsId=JobAttributeConfig(prompt=prompt),
      resource_name='Job',
      api_version='v1',
  )


def GetInstanceResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.namespaces.instances',
      namespacesId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      instancesId=InstanceAttributeConfig(prompt=prompt),
      resource_name='Instance',
      api_version='v1',
  )


def GetExecutionResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.namespaces.executions',
      namespacesId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      executionsId=ExecutionAttributeConfig(prompt=prompt),
      resource_name='Execution',
      api_version='v1',
  )


def GetTaskResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.namespaces.tasks',
      namespacesId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      tasksId=TaskAttributeConfig(prompt=prompt),
      resource_name='Task',
      api_version='v1',
  )


def GetV1WorkerPoolResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.namespaces.workerpools',
      namespacesId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      workerpoolsId=WorkerPoolAttributeConfig(prompt),
      resource_name='WorkerPool',
      api_version='v1',
  )


def GetV2WorkerPoolResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.projects.locations.workerPools',
      projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      locationsId=LocationAttributeConfig(),
      workerPoolsId=WorkerPoolAttributeConfig(prompt),
      resource_name='WorkerPool',
      api_version='v2',
  )


def GetV2WorkerPoolRevisionResourceSpec(prompt=False):
  return concepts.ResourceSpec(
      'run.projects.locations.workerPools.revisions',
      projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      locationsId=LocationAttributeConfig(),
      workerPoolsId=WorkerPoolAttributeConfig(prompt),
      revisionsId=RevisionAttributeConfig(),
      resource_name='WorkerPoolRevision',
      api_version='v2',
  )


def GetProjectResourceSpec():
  return concepts.ResourceSpec(
      'run.projects',
      resource_name='project',
      projectsId=ProjectAttributeConfig(),
  )


def GetRegionResourceSpec():
  return concepts.ResourceSpec(
      'run.projects.locations',
      projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
      locationsId=LocationAttributeConfig(),
      resource_name='Region',
      api_version='v2',
  )


def GetNamespaceResourceSpec():
  """Returns a resource spec for the namespace."""
  # TODO(b/148817410): Remove this when the api has been split.
  # This try/except block is needed because the v1alpha1 and v1 run apis
  # have different collection names for the namespaces.
  try:
    return concepts.ResourceSpec(
        'run.namespaces',
        namespacesId=NamespaceAttributeConfig(),
        resource_name='namespace',
    )
  except resources.InvalidCollectionException:
    return concepts.ResourceSpec(
        'run.api.v1.namespaces',
        namespacesId=NamespaceAttributeConfig(),
        resource_name='namespace',
    )


CLUSTER_PRESENTATION = presentation_specs.ResourcePresentationSpec(
    '--cluster',
    GetClusterResourceSpec(),
    'Kubernetes Engine cluster to connect to.',
    hidden=True,
    required=False,
    prefixes=True,
)

REGION_PRESENTATION = presentation_specs.ResourcePresentationSpec(
    '--region',
    GetRegionResourceSpec(),
    'Cloud region to use.',
    hidden=True,
    required=False,
    prefixes=True,
)
