# -*- 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.

"""Contains utilities to support the `gcloud init` command."""

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

from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.cloudresourcemanager import projects_util
from googlecloudsdk.api_lib.resource_manager import operations
from googlecloudsdk.calliope import usage_text
from googlecloudsdk.command_lib.projects import util as  projects_command_util
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.console import console_io
import six


_ENTER_PROJECT_ID_MESSAGE = """\
Enter a Project ID. Note that a Project ID CANNOT be changed later.
Project IDs must be 6-30 characters (lowercase ASCII, digits, or
hyphens) in length and start with a lowercase letter. \
"""
_CREATE_PROJECT_SENTINEL = object()

# At around 200 projects, being able to list them out becomes less useful.
_PROJECT_LIST_LIMIT = 200


def _GetProjectIds(limit=None):
  """Returns a list of project IDs the current user can list.

  Args:
    limit: int, the maximum number of project ids to return.

  Returns:
    list of str, project IDs, or None (if the command fails).
  """
  try:
    projects = projects_api.List(limit=limit)
    return sorted([project.projectId for project in projects])
  except Exception as err:  # pylint: disable=broad-except
    log.warning('Listing available projects failed: %s', six.text_type(err))
    return None


def _IsExistingProject(project_id):
  project_ref = projects_command_util.ParseProject(project_id)
  try:
    project = projects_api.Get(project_ref)
    return projects_util.IsActive(project)
  except Exception:  # pylint: disable=broad-except
    # Yeah, this isn't great, but there isn't a perfect exception super class
    # that covers both API related errors and network errors.
    return False


def _PromptForProjectId(project_ids, limit_exceeded):
  """Prompt the user for a project ID, based on the list of available IDs.

  Also allows an option to create a project.

  Args:
    project_ids: list of str or None, the project IDs to prompt for. If this
      value is None, the listing was unsuccessful and we prompt the user
      free-form (and do not validate the input). If it's empty, we offer to
      create a project for the user.
    limit_exceeded: bool, whether or not the project list limit was reached. If
      this limit is reached, then user will be prompted with a choice to
      manually enter a project id, create a new project, or list all projects.

  Returns:
    str, the project ID to use, or _CREATE_PROJECT_SENTINEL (if a project should
      be created), or None
  """
  if project_ids is None:
    return console_io.PromptResponse(
        'Enter project id you would like to use:  ') or None
  elif not project_ids:
    if not console_io.PromptContinue(
        'This account has no projects.',
        prompt_string='Would you like to create one?'):
      return None
    return _CREATE_PROJECT_SENTINEL
  elif limit_exceeded:
    idx = console_io.PromptChoice(
        ['Enter a project ID', 'Create a new project', 'List projects'],
        message=('This account has a lot of projects! Listing them all can '
                 'take a while.'))
    if idx is None:
      return None
    elif idx == 0:
      return console_io.PromptWithValidator(
          _IsExistingProject,
          'Project ID does not exist or is not active. Please enter an '
          'existing and active Project ID.',
          'Enter an existing project id you would like to use:  ')
    elif idx == 1:
      return _CREATE_PROJECT_SENTINEL
    else:
      project_ids = _GetProjectIds()

  idx = console_io.PromptChoice(
      project_ids + ['Create a new project'],
      message='Pick cloud project to use: ',
      allow_freeform=True,
      freeform_suggester=usage_text.TextChoiceSuggester())
  if idx is None:
    return None
  elif idx == len(project_ids):
    return _CREATE_PROJECT_SENTINEL
  return project_ids[idx]


def _CreateProject(project_id, project_ids):
  """Create a project and check that it isn't in the known project IDs."""
  if project_ids and project_id in project_ids:
    raise ValueError('Attempting to create a project that already exists.')

  project_ref = resources.REGISTRY.Create(
      'cloudresourcemanager.projects', projectId=project_id)
  try:
    create_op = projects_api.Create(project_ref)
  except Exception as err:  # pylint: disable=broad-except
    log.warning('Project creation failed: {err}\n'
                'Please make sure to create the project [{project}] using\n'
                '    $ gcloud projects create {project}\n'
                'or change to another project using\n'
                '    $ gcloud config set project <PROJECT ID>'.format(
                    err=six.text_type(err), project=project_id))
    return None
  # wait for async Create operation to complete and check the status.
  try:
    create_op = operations.WaitForOperation(create_op)
  except operations.OperationFailedException as err:
    log.warning(
        'Project creation for project [{project}] failed:\n  {err}'.format(
            err=six.text_type(err), project=project_id))
    return None
  return project_id


def PickProject(preselected=None):
  """Allows user to select a project.

  Args:
    preselected: str, use this value if not None

  Returns:
    str, project_id or None if was not selected.
  """
  project_ids = _GetProjectIds(limit=_PROJECT_LIST_LIMIT + 1)
  limit_exceeded = False
  if project_ids is not None and len(project_ids) > _PROJECT_LIST_LIMIT:
    limit_exceeded = True

  project_id = preselected or _PromptForProjectId(project_ids, limit_exceeded)
  if not limit_exceeded:
    if project_ids is None or project_id in project_ids or project_id is None:
      return project_id
  else:
    # If we fall into limit_exceeded logic and preselected was None, then
    # as long as project_id is not _CREATE_PROJECT_SENTINEL, then we know the
    # project_id is valid.
    if ((preselected and _IsExistingProject(preselected)) or
        project_id is not _CREATE_PROJECT_SENTINEL):
      return project_id

  if project_id is _CREATE_PROJECT_SENTINEL:
    project_id = console_io.PromptResponse(_ENTER_PROJECT_ID_MESSAGE)
    if not project_id:
      return None
  else:
    if project_ids:
      message = '[{0}] is not one of your projects [{1}]. '.format(
          project_id, ','.join(project_ids))
    else:
      message = 'This account has no projects.'
    if not console_io.PromptContinue(
        message=message, prompt_string='Would you like to create it?'):
      return None

  return _CreateProject(project_id, project_ids)
