# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Google OAuth2 related functions."""

from __future__ import print_function

import collections
import datetime
import functools
import httplib2
import json
import logging
import os

import subprocess2


# This is what most GAE apps require for authentication.
OAUTH_SCOPE_EMAIL = 'https://www.googleapis.com/auth/userinfo.email'
# Gerrit and Git on *.googlesource.com require this scope.
OAUTH_SCOPE_GERRIT = 'https://www.googleapis.com/auth/gerritcodereview'
# Deprecated. Use OAUTH_SCOPE_EMAIL instead.
OAUTH_SCOPES = OAUTH_SCOPE_EMAIL


# Mockable datetime.datetime.utcnow for testing.
def datetime_now():
  return datetime.datetime.utcnow()


# OAuth access token with its expiration time (UTC datetime or None if unknown).
class AccessToken(collections.namedtuple('AccessToken', [
    'token',
    'expires_at',
  ])):

  def needs_refresh(self):
    """True if this AccessToken should be refreshed."""
    if self.expires_at is not None:
      # Allow 30s of clock skew between client and backend.
      return datetime_now() + datetime.timedelta(seconds=30) >= self.expires_at
    # Token without expiration time never expires.
    return False


class LoginRequiredError(Exception):
  """Interaction with the user is required to authenticate."""

  def __init__(self, scopes=OAUTH_SCOPE_EMAIL):
    msg = (
        'You are not logged in. Please login first by running:\n'
        '  luci-auth login -scopes %s' % scopes)
    super(LoginRequiredError, self).__init__(msg)


def has_luci_context_local_auth():
  """Returns whether LUCI_CONTEXT should be used for ambient authentication."""
  ctx_path = os.environ.get('LUCI_CONTEXT')
  if not ctx_path:
    return False
  try:
    with open(ctx_path) as f:
      loaded = json.load(f)
  except (OSError, IOError, ValueError):
    return False
  return loaded.get('local_auth', {}).get('default_account_id') is not None


class Authenticator(object):
  """Object that knows how to refresh access tokens when needed.

  Args:
    scopes: space separated oauth scopes. Defaults to OAUTH_SCOPE_EMAIL.
  """

  def __init__(self, scopes=OAUTH_SCOPE_EMAIL):
    self._access_token = None
    self._scopes = scopes

  def has_cached_credentials(self):
    """Returns True if credentials can be obtained.

    If returns False, get_access_token() later will probably ask for interactive
    login by raising LoginRequiredError.

    If returns True, get_access_token() won't ask for interactive login.
    """
    return bool(self._get_luci_auth_token())

  def get_access_token(self):
    """Returns AccessToken, refreshing it if necessary.

    Raises:
      LoginRequiredError if user interaction is required.
    """
    if self._access_token and not self._access_token.needs_refresh():
      return self._access_token

    # Token expired or missing. Maybe some other process already updated it,
    # reload from the cache.
    self._access_token = self._get_luci_auth_token()
    if self._access_token and not self._access_token.needs_refresh():
      return self._access_token

    # Nope, still expired. Needs user interaction.
    logging.error('Failed to create access token')
    raise LoginRequiredError(self._scopes)

  def authorize(self, http):
    """Monkey patches authentication logic of httplib2.Http instance.

    The modified http.request method will add authentication headers to each
    request.

    Args:
       http: An instance of httplib2.Http.

    Returns:
       A modified instance of http that was passed in.
    """
    # Adapted from oauth2client.OAuth2Credentials.authorize.
    request_orig = http.request

    @functools.wraps(request_orig)
    def new_request(
        uri, method='GET', body=None, headers=None,
        redirections=httplib2.DEFAULT_MAX_REDIRECTS,
        connection_type=None):
      headers = (headers or {}).copy()
      headers['Authorization'] = 'Bearer %s' % self.get_access_token().token
      return request_orig(
          uri, method, body, headers, redirections, connection_type)

    http.request = new_request
    return http

  ## Private methods.

  def _run_luci_auth_login(self):
    """Run luci-auth login.

    Returns:
      AccessToken with credentials.
    """
    logging.debug('Running luci-auth login')
    subprocess2.check_call(['luci-auth', 'login', '-scopes', self._scopes])
    return self._get_luci_auth_token()

  def _get_luci_auth_token(self):
    logging.debug('Running luci-auth token')
    try:
      out, err = subprocess2.check_call_out(
          ['luci-auth', 'token', '-scopes', self._scopes, '-json-output', '-'],
          stdout=subprocess2.PIPE, stderr=subprocess2.PIPE)
      logging.debug('luci-auth token stderr:\n%s', err)
      token_info = json.loads(out)
      return AccessToken(
          token_info['token'],
          datetime.datetime.utcfromtimestamp(token_info['expiry']))
    except subprocess2.CalledProcessError:
      return None