try:
    from functools import lru_cache
except ImportError:
    from functools32 import lru_cache
import base64
import cgi
import datetime
import httplib2
import jinja2
import json
import messageindex
import os
import re
import time
import urllib
import webapp2


from google.appengine.api import app_identity
from google.appengine.api import users
from google.appengine.ext import ndb
from oauth2client.client import GoogleCredentials


JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.autoescape'],
    autoescape=True)

_IDENTITY_ENDPOINT = ('https://identitytoolkit.googleapis.com/'
                      'google.identity.identitytoolkit.v1.IdentityToolkit')
_FIREBASE_SCOPES = [
    'https://www.googleapis.com/auth/firebase.database',
    'https://www.googleapis.com/auth/userinfo.email']

DEFAULT_TOPIC = 'chat'

def messages_key():
    """Constructs a Datastore key for the Messages table.
    """
    return ndb.Key('Messages', 'Public')


def sessions_key():
    """Constructs a Datastore key for the Sessions table.
    """
    return ndb.Key('Sessions', 'All')


class Author(ndb.Model):
    """Sub model for representing an author."""
    identity = ndb.StringProperty(indexed=False)
    nickname = ndb.StringProperty(indexed=False)
    email = ndb.StringProperty(indexed=False)


class Message(ndb.Model):
    """A main model for representing an individual sent Message."""
    author = ndb.StructuredProperty(Author)
    # Note that the date is the only indexed property.  This is because this
    # table is only used for displaying the stream of messages, all searches are
    # done using the Search API:
    date = ndb.DateTimeProperty()
    topic = ndb.StringProperty(indexed=False)
    content = ndb.StringProperty(indexed=False)


class Session(ndb.Model):
    """A main model for representing an user's session."""
    client_id = ndb.StringProperty(indexed=True)
    # Not used, only for making administration easier:
    email = ndb.StringProperty(indexed=False)


def message_to_struct(message):
    """Transforms a Message into a simple structure for passing to HTML."""

    struct_message = {
        'id': cgi.escape(message.date.isoformat()),
        'nickname': cgi.escape(message.author.nickname),
        'email': cgi.escape(message.author.email),
        'date': cgi.escape(message.date.strftime('%x %X')),
        'topic': cgi.escape(message.topic),
        'content': cgi.escape(message.content).replace("\n", "<br>")
    }
    return struct_message


def create_custom_token(uid, valid_minutes=59):
    """Create a secure token for the given id.

    This method is used to create secure custom JWT tokens to be passed to
    clients. It takes a unique id (user_id) that will be used by Firebase's
    security rules to prevent unauthorized access.
    """

    # use the app_identity service from google.appengine.api to get the
    # project's service account email automatically
    client_email = app_identity.get_service_account_name()

    now = int(time.time())
    # encode the required claims
    # per https://firebase.google.com/docs/auth/server/create-custom-tokens
    payload = base64.b64encode(json.dumps({
        'iss': client_email,
        'sub': client_email,
        'aud': _IDENTITY_ENDPOINT,
        'uid': uid,  # the important parameter, as it will be the channel id
        'iat': now,
        'exp': now + (valid_minutes * 60),
    }))
    # add standard header to identify this as a JWT
    header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'}))
    to_sign = '{}.{}'.format(header, payload)
    # Sign the jwt using the built in app_identity service
    return '{}.{}'.format(to_sign, base64.b64encode(
        app_identity.sign_blob(to_sign)[1]))


class MainPage(webapp2.RequestHandler):
    """Generates the main web page."""

    def get(self):
        user = users.get_current_user()
        if not user:
            # This should never happen, as AppEngine should only run this
            # handler if the user is signed in.  But defense in depth applies...
            self.redirect(users.create_login_url(self.request.uri))
            return

        # If this user has not used the system before, add their user_id to the
        # table of IDs which we attempt to broadcast all messages to.
        #
        # Room for improvement: right now this table will grow endlessly as more
        # and more people use the system.  This may not scale if the system
        # becomes popular.  We actually only want a list of people with open
        # sessions.
        #
        # Idea: have a heartbeat from clients, and expire entries in this table
        # which have not gotten a heartbeat in a long time.  You might worry
        # that this server could be DoSed by getting too many heartbeats from a
        # large number of simultaneously active clients -- but this system is
        # already broadcasting to all active clients anyways, so we'll hit
        # scaling issues in the broadcast (which we'll have to solve) long
        # before we get DoSed by inbound heartbeats.
        query = Session.query(Session.client_id == user.user_id())
        if query.iter().has_next():
            session = query.iter().next()
        else:
            session = Session(parent=sessions_key())
            session.client_id = user.user_id();
            session.email = user.email();
            session.put()

        topic = self.request.get('topic', DEFAULT_TOPIC)

        # encrypt the channel_id and send it as a custom token to the
        # client
        # Firebase's data security rules will be able to decrypt the
        # token and prevent unauthorized access
        token = create_custom_token(session.client_id)

        template_values = {
            'user': user,
            'topic': urllib.quote_plus(topic),
            'token': token,
            'channel_id': user.user_id(),
        }

        template = JINJA_ENVIRONMENT.get_template('index.html')
        self.response.write(template.render(template_values))


def safeStrToInt(s):
    try:
        return int(s)
    except ValueError:
        return 10


class SearchPage(webapp2.RequestHandler):
    """Generates the search results page."""
    def get(self):
        self.post()

    def post(self):
        user = users.get_current_user()
        if not user:
            # This should never happen, as AppEngine should only run this
            # handler if the user is signed in.  But defense in depth applies...
            self.redirect(users.create_login_url(self.request.uri))
            return

        query = self.request.get('query', '')
        num_results = safeStrToInt(self.request.get('num_results', '10'))

        urlsafe_keys = messageindex.find(query, num_results)


        results = []
        for urlsafe_key in urlsafe_keys:
            result = ndb.Key(urlsafe=urlsafe_key).get()
            if result:
                results.append(message_to_struct(result))

        template_values = {
            'query': query,
            'num_results': num_results,
            'results': results
        }

        template = JINJA_ENVIRONMENT.get_template('search.html')
        self.response.write(template.render(template_values))


# Memoize the value, to avoid parsing the code snippet every time
@lru_cache()
def _get_firebase_db_url():
    """Grabs the databaseURL from the Firebase config snippet. Regex looks
    scary, but all it is doing is pulling the 'databaseURL' field from the
    Firebase javascript snippet"""
    regex = re.compile(r'\bdatabaseURL\b.*?["\']([^"\']+)')
    cwd = os.path.dirname(__file__)
    try:
        with open(os.path.join(cwd, 'index.html')) as f:
            url = next(regex.search(line) for line in f if regex.search(
                line))
    except StopIteration:
        raise ValueError(
            'Error parsing databaseURL. Please copy Firebase web snippet '
            'into index.html')
    return url.group(1)

# Memoize the authorized http, to avoid fetching new access tokens
@lru_cache()
def _get_http():
    """Provides an authed http object."""
    http = httplib2.Http()
    # Use application default credentials to make the Firebase calls
    # https://firebase.google.com/docs/reference/rest/database/user-auth
    creds = GoogleCredentials.get_application_default().create_scoped(
        _FIREBASE_SCOPES)
    creds.authorize(http)
    return http

class MessagesBroadcast():
    """Given an array of messages, broadcast it to all users who have opened the UI."""
    message = None

    def __init__(self, messages):
        self.messages = messages

    def encode_messages(self):
        struct_encoded = []
        for message in self.messages:
            struct_encoded.append(message_to_struct(message))
        return json.dumps(struct_encoded)

    def send_messages(self, dest):
        str_message = self.encode_messages()
        url = '{}/channels/{}.json'.format(_get_firebase_db_url(), dest)
        _get_http().request(url, 'PUT', body=str_message)

    def send(self):
        # Iterate over all logged in users and attempt to forward the message to
        # them:
        session_query = Session.query(ancestor=sessions_key())
        for session in session_query:
            self.send_messages(session.client_id)


class SendMessage(webapp2.RequestHandler):
    """Handler for the /send POST request."""
    def post(self):
        user = users.get_current_user()
        if not user:
            # This should never happen, as AppEngine should only run this
            # handler if the user is signed in.  But defense in depth applies...
            self.redirect(users.create_login_url(self.request.uri))
            return

        # Create a Message and store it in the DataStore.
        #
        # We set the same parent key on the 'Message' to ensure each Message is
        # in the same entity group. Queries across the single entity group will
        # be consistent. However, the write rate to a single entity group should
        # be limited to ~1/second.
        message = Message(parent=messages_key())

        topic = self.request.get('topic', DEFAULT_TOPIC)
        message.topic = topic
        message.author = Author(
                identity=user.user_id(),
                nickname=user.nickname(),
                email=user.email())
        message.content = self.request.get('content')
        message.date = datetime.datetime.now()
        message_key = message.put()

        # Index the message so it is available for future searches:
        messageindex.add(message_key.urlsafe(), message)

        # Now that we've recorded the message in the DataStore, broadcast it to
        # all open clients.
        broadcast = MessagesBroadcast([message])
        broadcast.send()


class GetMessages(webapp2.RequestHandler):
    """Handler for the /get POST request."""
    def post(self):
        user = users.get_current_user()
        if not user:
            # This should never happen, as AppEngine should only run this
            # handler if the user is signed in.  But defense in depth applies...
            self.redirect(users.create_login_url(self.request.uri))
            return

        older_than_id = self.request.get('older_than')
        older_than = datetime.datetime.strptime(older_than_id,
                                                "%Y-%m-%dT%H:%M:%S.%f")

        query = Message.query(ancestor=messages_key()).filter(Message.date <
                                                              older_than
        ).order(-Message.date)
        # Limit query to 50 messages:
        query_results = query.fetch(50)

        if len(query_results) > 0:
            broadcast = MessagesBroadcast(query_results)
            broadcast.send_messages(user.user_id())


app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/send', SendMessage),
    ('/get', GetMessages),
    ('/search', SearchPage),
], debug=True)