"""Google+ OAuth drop-in.

Google+ API docs: https://developers.google.com/+/api/latest/
Python API client docs: https://developers.google.com/api-client-library/python/

TODO: check that overriding CallbackHandler.finish() actually works.
"""
import json
import logging

import appengine_config

from apiclient import discovery
from apiclient.errors import HttpError
try:
  from oauth2client.appengine import CredentialsModel, OAuth2Decorator
except ImportError:
  from oauth2client.contrib.appengine import CredentialsModel, OAuth2Decorator
from oauth2client.client import OAuth2Credentials
from google.appengine.ext import db
from google.appengine.ext import ndb
import httplib2
from webutil import handlers as webutil_handlers
from webutil import util

import handlers
import models

# global
json_service = None


def init_json_service():
  global json_service
  if json_service is None:
    # service names and versions:
    # https://developers.google.com/api-client-library/python/apis/
    json_service = discovery.build('plus', 'v1', http=httplib2.Http())

# global. initialized in StartHandler.to_path().
oauth_decorator = None


class GooglePlusAuth(models.BaseAuth):
  """An authenticated Google+ user or page.

  Provides methods that return information about this user (or page) and make
  OAuth-signed requests to the Google+ API. Stores OAuth credentials in the
  datastore. See models.BaseAuth for usage details.

  Google+-specific details: implements http() and api() but not urlopen(). api()
  returns a apiclient.discovery.Resource. The datastore entity key name is the
  Google+ user id. Uses credentials from the stored CredentialsModel since
  google-api-python-client stores refresh tokens there.
  """
  user_json = ndb.TextProperty()
  creds_model = ndb.KeyProperty(kind='CredentialsModel')

  # deprecated. TODO: remove
  creds_json = ndb.TextProperty()

  def site_name(self):
    return 'Google+'

  def user_display_name(self):
    """Returns the user's name.
    """
    return json.loads(self.user_json)['displayName']

  def creds(self):
    """Returns an oauth2client.OAuth2Credentials.
    """
    if self.creds_model:
      return db.get(self.creds_model.to_old_key()).credentials
    else:
      # TODO: remove creds_json
      return OAuth2Credentials.from_json(self.creds_json)

  def access_token(self):
    """Returns the OAuth access token string.
    """
    return self.creds().access_token

  def http(self, **kwargs):
    """Returns an httplib2.Http that adds OAuth credentials to requests.
    """
    http = httplib2.Http(**kwargs)
    self.creds().authorize(http)
    return http

  def api(self):
    """Returns an apiclient.discovery.Resource for the Google+ JSON API.

    To use it, first choose a resource type (e.g. People), then make a call,
    then execute that call with an authorized Http instance. For example:

    gpa = GooglePlusAuth.get_by_id('123')
    results_json = gpa.people().search(query='ryan').execute(gpa.http())

    More details: https://developers.google.com/api-client-library/python/
    """
    init_json_service()
    return json_service


def handle_exception(self, e, debug):
  """Exception handler that passes back HttpErrors as real HTTP errors.
  """
  if isinstance(e, HttpError):
    logging.exception(e)
    self.response.set_status(e.resp.status)
    self.response.write(str(e))
  else:
    return webutil_handlers.handle_exception(self, e, debug)


class StartHandler(handlers.StartHandler, handlers.CallbackHandler):
  """Starts and finishes the OAuth flow. The decorator handles the redirects.
  """
  handle_exception = handle_exception

  # G+ scopes: https://developers.google.com/+/api/oauth#oauth-scopes
  DEFAULT_SCOPE = 'https://www.googleapis.com/auth/plus.me'

  @classmethod
  def to(cls, to_path, scopes=None):
    """Override this since we need to_path to instantiate the oauth decorator.
    """
    global oauth_decorator
    if oauth_decorator is None:
      oauth_decorator = OAuth2Decorator(
        client_id=appengine_config.GOOGLE_CLIENT_ID,
        client_secret=appengine_config.GOOGLE_CLIENT_SECRET,
        scope=cls.make_scope_str(scopes, separator=' '),
        callback_path=to_path,
        # make sure we ask for a refresh token so we can use it to get an access
        # token offline. requires prompt=consent! more:
        # ~/etc/google+_oauth_credentials_debugging_for_plusstreamfeed_bridgy
        # http://googleappsdeveloper.blogspot.com.au/2011/10/upcoming-changes-to-oauth-20-endpoint.html
        access_type='offline',
        prompt='consent',
        # https://developers.google.com/accounts/docs/OAuth2WebServer#incrementalAuth
        include_granted_scopes='true')

    class Handler(cls):
      @oauth_decorator.oauth_required
      def get(self):
        assert (appengine_config.GOOGLE_CLIENT_ID and
                appengine_config.GOOGLE_CLIENT_SECRET), (
          "Please fill in the google_client_id and google_client_secret files in "
          "your app's root directory.")

        # get the current user
        init_json_service()
        try:
          user = json_service.people().get(userId='me')\
              .execute(oauth_decorator.http())
        except BaseException, e:
          util.interpret_http_exception(e)
          raise
        logging.debug('Got one person: %r', user)

        store = oauth_decorator.credentials.store
        creds_model_key = ndb.Key(store._model.kind(), store._key_name)
        auth = GooglePlusAuth(id=user['id'],
                              creds_model=creds_model_key,
                              user_json=json.dumps(user))
        auth.put()
        self.finish(auth, state=self.request.get('state'))

      @oauth_decorator.oauth_required
      def post(self):
        return self.get()

    return Handler


class CallbackHandler(object):
  """OAuth callback handler factory.
  """
  @staticmethod
  def to(to_path):
    StartHandler.to_path = to_path
    global oauth_decorator
    assert oauth_decorator
    return oauth_decorator.callback_handler()
