# -*- coding: utf-8 -*-
"""
|gae| Extras
------------

Utilities you can use when using this library on |gae|_.
"""

import os

from google.appengine.ext import ndb
from webapp2_extras import sessions

from authomatic import exceptions
from authomatic.core import resolve_provider_class
from authomatic.extras import interfaces
from authomatic.providers import openid
from authomatic.extras.gae.openid import NDBOpenIDStore


__all__ = ['ndb_config', 'Webapp2Session']


class GAEError(exceptions.BaseError):
    pass


class Webapp2Session(interfaces.BaseSession):
    """
    A simple wrapper for |webapp2|_ sessions. If you provide a session
    it wraps it and adds the :meth:`.save` method.
    
    If you don't provide a session it creates a new one but you must provide the :data:`.secret`. 
    
    For more about |webapp2| sessions see:
    `http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html`_.
    """
    
    def __init__(self, handler, session=None, secret=None, cookie_name='webapp2authomatic',
                 backend='memcache', config=None):
        """
        
        .. warning::
            
            Do not use the ``'securecookie'`` backend with :class:`.providers.OpenID`
            provider. The `python-openid`_ library saves **non json serializable** objects to session
            which the ``'securecookie'`` backend cannot cope with.
            
        :param handler:
            A :class:`webapp2.RequestHandler` instance.
        
        :param session:
            A :class:`webapp2_extras.session.SessionDict` instance.
        
        :param str secret:
            The session secret.
        
        :param str cookie_name:
            The name of the session cookie.
        
        :param backend:
            The session backend. One of ``'memcache'`` or ``'datastore'``.
            
        :param config:
            The session config.
        """
        
        self.handler = handler
        
        if session is None:
            if not secret:
                raise GAEError('Either session or secret must be specified!')
            else:
                # Create new session.
                cfg = config or dict(secret_key=secret, cookie_name=cookie_name)
                session_store = sessions.SessionStore(handler.request, cfg)
                self.session_dict = session_store.get_session(backend=backend)
        else:
            # Use supplied session.
            self.session_dict = session
        
        
    def save(self):
        return self.session_dict.container.save_session(self.handler.response)
    
    
    def __setitem__(self, key, value):
        return self.session_dict.__setitem__(key, value)
    
    
    def __getitem__(self, key):
        return self.session_dict.__getitem__(key)
    
    
    def __delitem__(self, key):
        return self.session_dict.__delitem__(key)
    
    
    def get(self, key):
        return self.session_dict.get(key)
        

class NDBConfig(ndb.Model):
    """
    |gae| `NDB <https://developers.google.com/appengine/docs/python/ndb/>`_
    based :doc:`config`.
    
    .. note::
        
        By :class:`.OpenID` provider uses :class:`.NDBOpenIDStore` as default :attr:`.OpenID.store`.
        
    """
    
    # General properties
    provider_name = ndb.StringProperty( )
    class_ = ndb.StringProperty()
    
    # AuthorizationProvider properties
    provider_id = ndb.IntegerProperty()
    consumer_key = ndb.StringProperty()
    consumer_secret = ndb.StringProperty()
    
    # OAuth2 properties
    scope = ndb.StringProperty()
    offline = ndb.BooleanProperty()
    
    # AuthenticationProvider properties
    identifier_param = ndb.StringProperty()
    
    
    @classmethod
    def get(cls, key, default=None):
        """
        Resembles the :meth:`dict.get` method.
        
        :returns:
            A configuration dictionary for specified provider.
        """
        
        # Query datastore.
        result = cls.query(cls.provider_name == key).get()
        
        if result:
            result_dict = result.to_dict()
            
            # Use NDBOpenIDStore by default
            result_dict['store'] = NDBOpenIDStore
            
            # Convert coma-separated values to list. Currently only scope is csv.
            for i in ('scope', ):
                prop = result_dict.get(i)
                if prop:
                    result_dict[i] = [s.strip() for s in prop.split(',')]
                else:
                    result_dict[i] = None

            return result_dict
        else:
            return default
    
    
    @classmethod
    def values(cls):
        """
        Resembles the :meth:`dict.values` method.
        """
        
        # get all items
        results = cls.query().fetch()
        # return list of dictionaries
        return [result.to_dict() for result in results]
    
    
    @classmethod
    def initialize(cls):
        """
        Creates an **"Example"** entity of kind **"NDBConfig"** in the datastore
        if the model is empty and raises and error to inform you that you should populate the model with data.
        
        .. note::
        
            The *Datastore Viewer* in the ``_ah/admin/`` won't let you add properties to a model
            if there is not an entity with that property already.
            Therefore it is a good idea to keep the **"Example"** entity (which has all
            possible properties set) in the datastore.
        """
        
        if not len(cls.query().fetch()):
            
            example = cls.get_or_insert('Example')
            
            example.class_ = 'Provider class e.g. "authomatic.providers.oauth2.Facebook".'
            example.provider_name = 'Your custom provider name e.g. "fb".'
            
            # AuthorizationProvider
            example.consumer_key = 'Consumer key.'
            example.consumer_secret = 'Consumer secret'
            example.provider_id = 1
            
            # OAuth2
            example.scope = 'coma, separated, list, of, scopes'
            
            # AuthenticationProvider
            example.identifier_param = 'Querystring parameter for claimed id. default is "id"'
            
            # Save the example
            example.put()
            
            # Raise an information error.
            raise GAEError('A NDBConfig data model was created! Go to Datastore Viewer ' + \
                           'in your dashboard and populate it with data!')


def ndb_config():
    """
    Allows you to have a **datastore** :doc:`config` instead of a hardcoded one.
    
    This function creates an **"Example"** entity of kind **"NDBConfig"** in the datastore
    if the model is empty and raises and error to inform you that you should populate the model with data.
        
    .. note::
    
        The *Datastore Viewer* of the |gae|_ admin won't let you add properties to a model
        if there is not an entity with that property already.
        Therefore it is a good idea to keep the **"Example"** entity (which has all
        properties set) in the datastore.
    
    :raises:
        :exc:`.GAEError`
    
    :returns:
        :class:`.NDBConfig`
    """
    
    NDBConfig.initialize()
    return NDBConfig



