# Copyright 2006 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.

"""The User Python datastore class to be used as a datastore data type."""

# WARNING: This file is externally viewable by our users.  All comments from
# this file will be stripped.  The docstrings will NOT.  Do not put sensitive
# information in docstrings.  If you must communicate internal information in
# this source file, please place them in comments only.


from __future__ import absolute_import
__author__ = ['jonmac@google.com (Jon McAlister)',
              'ryanb@google.com (Ryan Barrett)']

# See user_service.proto.

import os
from googlecloudsdk.appengine._internal import six_subset
from googlecloudsdk.appengine.api import apiproxy_stub_map
from googlecloudsdk.appengine.api import user_service_pb
from googlecloudsdk.appengine.runtime import apiproxy_errors


class Error(Exception):
  """Base User error type."""


class UserNotFoundError(Error):
  """No email argument was specified, and no user is logged in."""


class RedirectTooLongError(Error):
  """The generated redirect URL was too long."""


class NotAllowedError(Error):
  """The requested redirect URL is not allowed."""


class User(object):
  """Provides the email address, nickname, and ID for a user.

  A nickname is a human-readable string that uniquely identifies a Google user,
  akin to a username. For some users, this nickname is an email address, but for
  other users, a different nickname is used.

  A user is a Google Accounts user.

  `federated_identity` and `federated_provider` are decommissioned and should
  not be used.
  """

  # For more information, go to http://g3doc/apphosting/g3doc/wiki-carryover/unified_theory_of_users.md

  # If any new properties are added to the object, also add them as class
  # properties here for backwards compat with pickled instances.
  __user_id = None
  __federated_identity = None
  __federated_provider = None

  def __init__(self, email=None, _auth_domain=None,
               _user_id=None, federated_identity=None, federated_provider=None,
               _strict_mode=True):
    """Constructor.

    Args:
      email: An optional string of the user's email address. It defaults to
          the current user's email address.
      federated_identity: Decommissioned, don't use.
      federated_provider: Decommissioned, don't use.

    Raises:
      UserNotFoundError: If the user is not logged in and both `email` and
          `federated_identity` are empty.
    """
    # the `_auth_domain argument` is intentionally not publicly documented. It's
    # only intended to be used by the admin console and other trusted apps.
    # The datastore backend will prevent untrusted apps from using it.
    # Similarly, the `_strict_mode` argument is not publicly documented as it is
    # only meant to be used by the `datastore_types` library in order to be more
    # forgiving when delivering data back to the user in case of data corruption
    # that may not pass the constructor validation.
    if _auth_domain is None:
      _auth_domain = os.environ.get('AUTH_DOMAIN')
    assert _auth_domain

    if email is None and federated_identity is None:
      email = os.environ.get('USER_EMAIL', email)
      _user_id = os.environ.get('USER_ID', _user_id)
      federated_identity = os.environ.get('FEDERATED_IDENTITY',
                                          federated_identity)
      federated_provider = os.environ.get('FEDERATED_PROVIDER',
                                          federated_provider)

    # We set this to maintain compatibility with the
    # `datastore_types.FromPropertyPb` creation of a User object, which will set
    # an empty string for the email (since it is a required field of the
    # underlying data representation of this class in the datastore.
    if email is None:
      email = ''

    if not email and not federated_identity and _strict_mode:
      # If no valid `email` and no valid `federated_identity`,
      # the user is not valid user.
      raise UserNotFoundError

    self.__email = email
    self.__federated_identity = federated_identity
    self.__federated_provider = federated_provider
    self.__auth_domain = _auth_domain
    self.__user_id = _user_id or None  # User.user_id() should only return a
    # string of length > 0 or None.

  def nickname(self):
    """Returns the user's nickname.

    The nickname will be a unique, human readable identifier for this user with
    respect to this application. It will be an email address for some users,
    and part of the email address for some users.

    Returns:
      The nickname of the user as a string.
    """
    if (self.__email and self.__auth_domain and
        self.__email.endswith('@' + self.__auth_domain)):
      suffix_len = len(self.__auth_domain) + 1
      return self.__email[:-suffix_len]
    elif self.__federated_identity:
      return self.__federated_identity
    else:
      return self.__email

  def email(self):
    """Returns the user's email address."""
    return self.__email

  def user_id(self):
    """Obtains the user ID of the user.

    Returns:
      A permanent unique identifying string or `None`. If the email address was
      set explicitly, this will return `None`.
    """
    return self.__user_id

  def auth_domain(self):
    """Obtains the user's authentication domain.

    Returns:
      A string containing the authentication domain. This method is internal and
      should not be used by client applications.
    """
    return self.__auth_domain

  def federated_identity(self):
    """Decommissioned, don't use.

    Returns:
      A string containing the federated identity of the user. If the user is not
      a federated user, `None` is returned.
    """
    return self.__federated_identity

  def federated_provider(self):
    """Decommissioned, don't use.

    Returns:
      A string containing the federated provider. If the user is not a federated
      user, `None` is returned.
    """
    return self.__federated_provider

  def __unicode__(self):
    return six_subset.text_type(self.nickname())

  def __str__(self):
    return str(self.nickname())

  def __repr__(self):
    values = []
    if self.__email:
      values.append("email='%s'" % self.__email)
    if self.__federated_identity:
      values.append("federated_identity='%s'" % self.__federated_identity)
    if self.__user_id:
      values.append("_user_id='%s'" % self.__user_id)
    return 'users.User(%s)' % ','.join(values)

  def __hash__(self):
    if self.__federated_identity:
      return hash((self.__federated_identity, self.__auth_domain))
    else:
      return hash((self.__email, self.__auth_domain))

  def __cmp__(self, other):
    if not isinstance(other, User):
      return NotImplemented
    if self.__federated_identity:
      return cmp((self.__federated_identity, self.__auth_domain),
                 (other.__federated_identity, other.__auth_domain))
    else:
      return cmp((self.__email, self.__auth_domain),
                 (other.__email, other.__auth_domain))


def create_login_url(dest_url=None, _auth_domain=None,
                     federated_identity=None):
  """Computes the login URL for redirection.

  Args:
    dest_url: String that is the desired final destination URL for the user
        once login is complete. If `dest_url` does not specify a host, the host
        from the current request is used.
    federated_identity: Decommissioned, don't use. Setting this to a non-None
        value raises a NotAllowedError

  Returns:
       Login URL as a string. The login URL will use Google Accounts.

  Raises:
      NotAllowedError: If federated_identity is not None.
  """
  req = user_service_pb.CreateLoginURLRequest()
  resp = user_service_pb.CreateLoginURLResponse()
  if dest_url:
    req.set_destination_url(dest_url)
  else:
    req.set_destination_url('')
  if _auth_domain:
    req.set_auth_domain(_auth_domain)
  if federated_identity:
    raise NotAllowedError('OpenID 2.0 support is decomissioned')

  try:
    apiproxy_stub_map.MakeSyncCall('user', 'CreateLoginURL', req, resp)
  except apiproxy_errors.ApplicationError as e:
    if (e.application_error ==
        user_service_pb.UserServiceError.REDIRECT_URL_TOO_LONG):
      raise RedirectTooLongError
    elif (e.application_error ==
          user_service_pb.UserServiceError.NOT_ALLOWED):
      raise NotAllowedError
    else:
      raise e
  return resp.login_url()

# Backwards compatible alias.  TODO(user): kill this.
CreateLoginURL = create_login_url


# `_auth_domain` will remain undocumented for now; external users can't provide
# it as an argument
# pylint: disable=g-doc-args
def create_logout_url(dest_url, _auth_domain=None):
  """Computes the logout URL and specified destination URL for the request.

  This function works for Google Accounts applications.

  Args:
    dest_url: String that is the desired final destination URL for the user
        after the user has logged out. If `dest_url` does not specify a host,
        the host from the current request is used.

  Returns:
    Logout URL as a string.
  """
  req = user_service_pb.CreateLogoutURLRequest()
  resp = user_service_pb.CreateLogoutURLResponse()
  req.set_destination_url(dest_url)
  if _auth_domain:
    req.set_auth_domain(_auth_domain)

  try:
    apiproxy_stub_map.MakeSyncCall('user', 'CreateLogoutURL', req, resp)
  except apiproxy_errors.ApplicationError as e:
    if (e.application_error ==
        user_service_pb.UserServiceError.REDIRECT_URL_TOO_LONG):
      raise RedirectTooLongError
    else:
      raise e
  return resp.logout_url()

# Backwards compatible alias.  TODO(user): kill this.
CreateLogoutURL = create_logout_url


def get_current_user():
  """Retrieves information associated with the user that is making a request.

  Returns:

  """
  try:
    return User()
  except UserNotFoundError:
    return None

# Backwards compatible alias.  TODO(user): kill this.
GetCurrentUser = get_current_user


def is_current_user_admin():
  """Specifies whether the user making a request is an application admin.

  Because administrator status is not persisted in the datastore,
  `is_current_user_admin()` is a separate function rather than a member function
  of the `User` class. The status only exists for the user making the current
  request.

  Returns:
    `True` if the user is an administrator; all other user types return `False`.
  """
  return (os.environ.get('USER_IS_ADMIN', '0')) == '1'

# Backwards compatible alias.  TODO(user): kill this.
IsCurrentUserAdmin = is_current_user_admin
