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

"""A command to install Application Default Credentials using a user account."""


import textwrap

from googlecloudsdk.api_lib.auth import util as auth_util
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as c_exc
from googlecloudsdk.command_lib.auth import auth_util as command_auth_util
from googlecloudsdk.command_lib.auth import flags
from googlecloudsdk.command_lib.auth import workforce_login_config as workforce_login_config_util
from googlecloudsdk.core import config
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.credentials import creds as creds_module
from googlecloudsdk.core.credentials import gce as c_gce
from googlecloudsdk.core.credentials import store as c_store
from googlecloudsdk.core.util import files


_DEPRECATED_SCOPES = frozenset([  # gcloud-disable-gdu-domain
    'https://www.googleapis.com/auth/drive',
    'https://www.googleapis.com/auth/spreadsheets',
    'https://www.googleapis.com/auth/drive.readonly',
    'https://www.googleapis.com/auth/analytics.readonly',
    'https://www.googleapis.com/auth/spreadsheets.readonly',
    'https://www.googleapis.com/auth/calendar',
    'https://www.googleapis.com/auth/adwords',
    'https://www.googleapis.com/auth/calendar.readonly',
    'https://www.googleapis.com/auth/analytics.edit',
    'https://www.googleapis.com/auth/drive.metadata.readonly',
    'https://www.googleapis.com/auth/content',
    'https://www.googleapis.com/auth/analytics',
    'https://www.googleapis.com/auth/contacts.readonly',
    'https://www.googleapis.com/auth/drive.metadata',
    'https://www.googleapis.com/auth/contacts',
    'https://www.googleapis.com/auth/youtube',
    'https://www.googleapis.com/auth/calendar.events.readonly',
    'https://www.googleapis.com/auth/drive.scripts',
    'https://www.googleapis.com/auth/docs',
    'https://www.googleapis.com/auth/drive.photos.readonly',
])


_TROUBLESHOOT_ADC_URL = (
    'https://docs.cloud.google.com/docs/authentication/'
    'troubleshoot-adc#access_blocked_when_using_scopes'
)


@base.UniverseCompatible
class Login(base.Command):
  r"""Acquire new user credentials to use for Application Default Credentials.

  Obtains user access credentials via a web flow and puts them in the
  well-known location for Application Default Credentials (ADC).

  This command is useful when you are developing code that would normally
  use a service account but need to run the code in a local development
  environment where it's easier to provide user credentials. The credentials
  will apply to all API calls that make use of the Application Default
  Credentials client library. Do not set the `GOOGLE_APPLICATION_CREDENTIALS`
  environment variable if you want to use the credentials generated by this
  command in your local development. This command tries to find a quota
  project from gcloud's context and write it to ADC so that Google client
  libraries can use it for billing and quota. Alternatively, you can use
  the `--client-id-file` flag. In this case, the project owning the client ID
  will be used for billing and quota. You can create the client ID file
  at https://console.cloud.google.com/apis/credentials.

  This command has no effect on the user account(s) set up by the
  `gcloud auth login` command.

  Any credentials previously generated by
  `gcloud auth application-default login` will be overwritten.
  """
  detailed_help = {
      'EXAMPLES':
          """\
          If you want your local application to temporarily use your own user
          credentials for API access, run:

            $ {command}

          If you'd like to login by passing in a file containing your own client
          id, run:

            $ {command} --client-id-file=clientid.json
          """
  }

  @staticmethod
  def Args(parser):
    """Set args for gcloud auth application-default login."""
    parser.add_argument(
        '--client-id-file',
        help='A file containing your own client id to use to login. If '
        '--client-id-file is specified, the quota project will not be '
        'written to ADC.')
    parser.add_argument(
        '--scopes',
        type=arg_parsers.ArgList(min_length=1),
        metavar='SCOPE',
        help='The names of the scopes to authorize for. By default '
        '{0} scopes are used. '
        'The list of possible scopes can be found at: '
        '[](https://developers.google.com/identity/protocols/googlescopes). '
        'To add scopes for applications outside of Google Cloud Platform, '
        'such as Google Drive, [create an OAuth Client ID]'
        '(https://support.google.com/cloud/answer/6158849) and provide it by '
        'using the --client-id-file flag. '
        .format(', '.join(auth_util.DEFAULT_SCOPES)))
    parser.add_argument(
        '--login-config',
        help='Path to the login configuration file (workforce pool, '
        'generated by the Cloud Console or '
        '`gcloud iam workforce-pools create-login-config`)',
        action=actions.StoreProperty(properties.VALUES.auth.login_config_file))
    parser.add_argument(
        'account',
        nargs='?',
        help=(
            'User account used for authorization. When the account specified'
            ' has valid credentials in the local credential store these'
            ' credentials will be re-used. Otherwise new ones will be fetched'
            ' and replace any stored credential.'
            ' This caching behavior is only available for user credentials.'
        ),
    )
    flags.AddQuotaProjectFlags(parser)
    flags.AddRemoteLoginFlags(parser, for_adc=True)

    parser.display_info.AddFormat('none')

  def Run(self, args):
    """Run the authentication command."""
    # TODO(b/203102970): Remove this condition check after the bug is resolved
    if properties.VALUES.auth.access_token_file.Get():
      raise c_store.FlowError(
          'auth/access_token_file or --access-token-file was set which is not '
          'compatible with this command. Please unset the property and rerun '
          'this command.'
      )

    if c_gce.Metadata().connected:
      message = textwrap.dedent("""
          You are running on a Google Compute Engine virtual machine.
          The service credentials associated with this virtual machine
          will automatically be used by Application Default
          Credentials, so it is not necessary to use this command.

          If you decide to proceed anyway, your user credentials may be visible
          to others with access to this virtual machine. Are you sure you want
          to authenticate with your personal account?
          """)
      console_io.PromptContinue(
          message=message, throw_if_unattended=True, cancel_on_no=True)

    command_auth_util.PromptIfADCEnvVarIsSet()
    if args.client_id_file and not args.launch_browser:
      raise c_exc.InvalidArgumentException(
          '--no-launch-browser',
          '`--no-launch-browser` flow no longer works with the '
          '`--client-id-file`. Please replace `--no-launch-browser` with '
          '`--no-browser`.'
      )

    # Currently the original scopes are not stored in the ADC. If they were, a
    # change in scopes could also be used to determine a cache hit.
    if args.account and not args.scopes:
      if ShouldUseCachedCredentials(args.account):
        log.warning(
            '\nValid credentials already exist for {}. To force refresh the'
            ' existing credentials, omit the "ACCOUNT" positional field.'
            .format(args.account)
        )
        return

    scopes = args.scopes or auth_util.DEFAULT_SCOPES

    target_impersonation_principal, delegates, target_scopes = None, None, None
    impersonation_service_accounts = (
        properties.VALUES.auth.impersonate_service_account.Get()
    )
    if impersonation_service_accounts:
      (target_impersonation_principal, delegates
      ) = c_store.ParseImpersonationAccounts(impersonation_service_accounts)
      # Only set target_scopes if user explicitly asked for something, if not
      # we will let auth and client library decide on the scopes.
      target_scopes = args.scopes or None
      # Source credential just needs the scope needed to call the IAM API for
      # impersonation. Asking user to consent any other scope is unnecessary.
      scopes = [auth_util.CLOUD_PLATFORM_SCOPE]

    # Check for deprecated scopes
    deprecated_scopes_found = _DEPRECATED_SCOPES.intersection(scopes)
    if deprecated_scopes_found and not args.client_id_file:
      log.warning(
          '\nThe following scopes will be blocked soon for'
          ' the default client ID:'
          ' {}.\n'
          'To use these scopes, you must provide your own client ID or use '
          'service account impersonation.\n'
          'Please refer to the documentation for more information: '
          '{}'.format(
              ', '.join(sorted(deprecated_scopes_found)), _TROUBLESHOOT_ADC_URL
          )
      )

    flow_params = dict(
        no_launch_browser=not args.launch_browser,
        no_browser=not args.browser,
        remote_bootstrap=args.remote_bootstrap,
    )

    # 1. Try the 3PI web flow with --no-browser:
    #    This could be a 3PI flow initiated via --no-browser.
    #    If provider_name is present, then this is the 3PI flow.
    #    We can start the flow as is as the remote_bootstrap value will be used.
    if args.remote_bootstrap and 'provider_name' in args.remote_bootstrap:
      auth_util.DoInstalledAppBrowserFlowGoogleAuth(
          config.CLOUDSDK_EXTERNAL_ACCOUNT_SCOPES,
          auth_proxy_redirect_uri=(
              'https://sdk.cloud.google/applicationdefaultauthcode.html'
          ),
          **flow_params
      )
      return

    # 2. Try the 3PI web flow with a login configuration file.
    login_config_file = workforce_login_config_util.GetWorkforceLoginConfig()
    if login_config_file:
      if args.client_id_file:
        raise c_exc.ConflictingArgumentsException(
            '--client-id-file is not currently supported for third party login '
            'flows. ')
      if args.scopes:
        raise c_exc.ConflictingArgumentsException(
            '--scopes is not currently supported for third party login flows.')
      creds = workforce_login_config_util.DoWorkforceHeadfulLogin(
          login_config_file,
          True,
          **flow_params
      )
    else:
      # 3. Try the 1P web flow.
      properties.VALUES.auth.client_id.Set(
          auth_util.DEFAULT_CREDENTIALS_DEFAULT_CLIENT_ID)
      properties.VALUES.auth.client_secret.Set(
          auth_util.DEFAULT_CREDENTIALS_DEFAULT_CLIENT_SECRET)
      if auth_util.CLOUD_PLATFORM_SCOPE not in scopes:
        raise c_exc.InvalidArgumentException(
            '--scopes',
            '{} scope is required but not requested. Please include it in the'
            ' --scopes flag.'.format(auth_util.CLOUD_PLATFORM_SCOPE),
        )
      creds = auth_util.DoInstalledAppBrowserFlowGoogleAuth(
          scopes,
          client_id_file=args.client_id_file,
          auth_proxy_redirect_uri=(
              'https://sdk.cloud.google.com/applicationdefaultauthcode.html'
          ),
          **flow_params
      )
    if not creds:
      return

    if args.account and hasattr(creds, 'with_account'):
      _ = command_auth_util.ExtractAndValidateAccount(args.account, creds)
      creds = creds.with_account(args.account)

    if not target_impersonation_principal:
      if args.IsSpecified('client_id_file'):
        command_auth_util.DumpADC(creds, quota_project_disabled=False)
      elif args.disable_quota_project:
        command_auth_util.DumpADC(creds, quota_project_disabled=True)
      else:
        command_auth_util.DumpADCOptionalQuotaProject(creds)
    else:
      # TODO(b/184049366): Supports quota project with impersonated creds.
      command_auth_util.DumpImpersonatedServiceAccountToADC(
          creds,
          target_principal=target_impersonation_principal,
          delegates=delegates,
          scopes=target_scopes)
    return creds


def ShouldUseCachedCredentials(account):
  """Skip login if the existing ADC was provisioned for the requested account."""
  try:
    file_path = config.ADCFilePath()
    data = files.ReadFileContents(file_path)
  except files.Error:
    return False

  try:
    credential = creds_module.FromJsonGoogleAuth(data)
  except creds_module.UnknownCredentialsType:
    return False
  except creds_module.InvalidCredentialsError:
    return False

  cred_type = creds_module.CredentialTypeGoogleAuth.FromCredentials(credential)
  if cred_type == creds_module.CredentialTypeGoogleAuth.USER_ACCOUNT:
    if credential.account != account:
      return False

  return True
