'''
Allura plugins for authentication and project registration
'''
import logging
import re

from random import randint
from hashlib import sha256
from base64 import b64encode
from datetime import datetime

import ldap
import pkg_resources
from tg import config
from pylons import g, c
from webob import exc

from ming.utils import LazyProperty
from ming.orm import session
from ming.orm import ThreadLocalORMSession

from allura.lib import helpers as h
from allura.lib import exceptions as forge_exc

log = logging.getLogger(__name__)

class AuthenticationProvider(object):

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

    @classmethod
    def get(cls, request):
        method = config.get('auth.method', 'local')
        for ep in pkg_resources.iter_entry_points('allura.auth', method):
            return ep.load()(request)
        return None

    @LazyProperty
    def session(self):
        return self.request.environ['beaker.session']

    def authenticate_request(self):
        from allura import model as M
        user = M.User.query.get(_id=self.session.get('userid', None))
        if user is None:
            return M.User.anonymous()
        return user

    def register_user(self, user_doc):
        raise NotImplementedError, 'register_user'

    def _login(self):
        raise NotImplementedError, '_login'

    def login(self, user=None):
        try:
            if user is None: user = self._login()
            self.session['userid'] = user._id
            self.session.save()
            return user
        except exc.HTTPUnauthorized:
            self.logout()
            raise

    def logout(self):
        self.session['userid'] = None
        self.session.save()

    def by_username(self, username):
        raise NotImplementedError, 'by_username'

    def set_password(self, user, old_password, new_password):
        raise NotImplementedError, 'set_password'

class LocalAuthenticationProvider(AuthenticationProvider):

    def register_user(self, user_doc):
        from allura import model as M
        return M.User(**user_doc)

    def _login(self):
        user = self.by_username(self.request.params['username'])
        if not self._validate_password(user, self.request.params['password']):
            raise exc.HTTPUnauthorized()
        return user

    def _validate_password(self, user, password):
        if user is None: return False
        if not user.password: return False
        salt = str(user.password[6:6+user.SALT_LEN])
        check = self._encode_password(password, salt)
        if check != user.password: return False
        return True

    def by_username(self, username):
        from allura import model as M
        un = re.escape(username)
        un = un.replace(r'\_', '[-_]')
        un = un.replace(r'\-', '[-_]')
        rex = re.compile('^' + un + '$')
        return M.User.query.get(username=rex)

    def set_password(self, user, old_password, new_password):
        user.password = self._encode_password(new_password)

    def _encode_password(self, password, salt=None):
        from allura import model as M
        if salt is None:
            salt = ''.join(chr(randint(1, 0x7f))
                           for i in xrange(M.User.SALT_LEN))
        hashpass = sha256(salt + password.encode('utf-8')).digest()
        return 'sha256' + salt + b64encode(hashpass)

class LdapAuthenticationProvider(AuthenticationProvider):

    def register_user(self, user_doc):
        from allura import model as M
        password = user_doc.pop('password', None)
        result = M.User(**user_doc)
        dn = 'uid=%s,%s' % (user_doc['username'], config['auth.ldap.suffix'])
        try:
            con = ldap.initialize(config['auth.ldap.server'])
            con.bind_s(config['auth.ldap.admin_dn'],
                       config['auth.ldap.admin_password'])
            ldap_info = dict(
                uid=user_doc['username'],
                displayName=user_doc['display_name'],
                cn=user_doc['display_name'],
                userPassword=password,
                objectClass=['inetOrgPerson'],
                givenName=user_doc['display_name'].split()[0],
                sn=user_doc['display_name'].split()[-1])
            ldap_info = dict((k,v) for k,v in ldap_info.iteritems()
                             if v is not None)
            try:
                con.add_s(dn, ldap_info.items())
            except ldap.ALREADY_EXISTS:
                con.modify_s(dn, [(ldap.MOD_REPLACE, k, v)
                                  for k,v in ldap_info.iteritems()])
            con.unbind_s()
        except:
            raise
        return result

    def by_username(self, username):
        from allura import model as M
        return M.User.query.get(username=username)

    def set_password(self, user, old_password, new_password):
        try:
            dn = 'uid=%s,%s' % (self.username, config['auth.ldap.suffix'])
            con = ldap.initialize(config['auth.ldap.server'])
            con.bind_s(dn, old_password)
            con.modify_s(dn, [(ldap.MOD_REPLACE, 'userPassword', new_password)])
            con.unbind_s()
        except ldap.INVALID_CREDENTIALS:
            raise exc.HTTPUnauthorized()

    def _login(self):
        from allura import model as M
        user = M.User.query.get(username=self.request.params['username'])
        if user is None: raise exc.HTTPUnauthorized()
        try:
            dn = 'uid=%s,%s' % (user.username, config['auth.ldap.suffix'])
            con = ldap.initialize(config['auth.ldap.server'])
            con.bind_s(dn, self.request.params['password'])
            con.unbind_s()
        except ldap.INVALID_CREDENTIALS:
            raise exc.HTTPUnauthorized()
        return user

class ProjectRegistrationProvider(object):

    @classmethod
    def get(cls):
        method = config.get('registration.method', 'local')
        for ep in pkg_resources.iter_entry_points('allura.project_registration', method):
            return ep.load()()

    def register_neighborhood_project(self, neighborhood, users):
        from allura import model as M
        shortname='--init--'
        p = M.Project.query.get(
            neighborhood_id=neighborhood._id,
            shortname=shortname)
        if p: raise forge_exc.ProjectConflict()
        database = 'project%s__init__' % neighborhood.url_prefix
        database = database.replace('/', ':').replace('-', '_')
        name = 'Home Project for %s' % neighborhood.name
        p = M.Project(neighborhood_id=neighborhood._id,
                    shortname=shortname,
                    name=name,
                    short_description='',
                    description=('# ' + name + '\n\n'
                                 + 'You can edit this description in the admin page'),
                    database=database,
                    last_updated = datetime.utcnow(),
                    is_root=True)
        try:
            p.configure_project_database()
            with h.push_config(c, project=p):
                assert M.ProjectRole.query.find().count() == 0, \
                    'Project roles already exist'
                # Install default named roles (#78)
                role_owner = M.ProjectRole(name='Admin')
                role_developer = M.ProjectRole(name='Developer')
                role_member = M.ProjectRole(name='Member')
                role_auth = M.ProjectRole(name='*authenticated')
                role_anon = M.ProjectRole(name='*anonymous')
                # Setup subroles
                role_owner.roles = [ role_developer._id ]
                role_developer.roles = [ role_member._id ]
                p.acl['create'] = [ role_owner._id ]
                p.acl['read'] = [ role_owner._id, role_developer._id, role_member._id,
                                  role_anon._id ]
                p.acl['update'] = [ role_owner._id ]
                p.acl['delete'] = [ role_owner._id ]
                p.acl['tool'] = [ role_owner._id ]
                p.acl['security'] = [ role_owner._id ]
                for user in users:
                    with h.push_config(c, user=user):
                        pr = user.project_role()
                        pr.roles = [ role_owner._id, role_developer._id, role_member._id ]
                        # Setup builtin tool applications
                with h.push_config(c, user=users[0]):
                    p.install_app('wiki', 'home')
                    p.install_app('admin', 'admin')
                ThreadLocalORMSession.flush_all()
        except:
            ThreadLocalORMSession.close_all()
            log.exception('Error registering project, attempting to drop %s',
                          database)
            try:
                session(p).impl.bind._conn.drop_database(database)
            except:
                log.exception('Error dropping database %s', database)
                pass
            raise
        return p

    def register_project(self, neighborhood, shortname, user, user_project):
        '''Register a new project in the neighborhood.  The given user will
        become the project's superuser.  If no user is specified, c.user is used.
        '''
        from allura import model as M
        assert h.re_path_portion.match(shortname.replace('/', '')), \
            'Invalid project shortname'
        p = M.Project.query.get(shortname=shortname)
        if p: raise forge_exc.ProjectConflict()
        database = 'project:' + shortname.replace('/', ':').replace('-', '_')
        p = M.Project(neighborhood_id=neighborhood._id,
                    shortname=shortname,
                    name=shortname,
                    short_description='',
                    description=(shortname + '\n'
                                 + '=' * 80 + '\n\n'
                                 + 'You can edit this description in the admin page'),
                    database=database,
                    last_updated = datetime.utcnow(),
                    is_root=True)
        try:
            p.configure_project_database()
            with h.push_config(c, project=p, user=user):
                assert M.ProjectRole.query.find().count() == 0, \
                    'Project roles already exist'
                # Install default named roles (#78)
                role_owner = M.ProjectRole(name='Admin')
                role_developer = M.ProjectRole(name='Developer')
                role_member = M.ProjectRole(name='Member')
                role_auth = M.ProjectRole(name='*authenticated')
                role_anon = M.ProjectRole(name='*anonymous')
                # Setup subroles
                role_owner.roles = [ role_developer._id ]
                role_developer.roles = [ role_member._id ]
                p.acl['create'] = [ role_owner._id ]
                p.acl['read'] = [ role_owner._id, role_developer._id, role_member._id,
                                  role_anon._id ]
                p.acl['update'] = [ role_owner._id ]
                p.acl['delete'] = [ role_owner._id ]
                p.acl['tool'] = [ role_owner._id ]
                p.acl['security'] = [ role_owner._id ]
                pr = user.project_role()
                pr.roles = [ role_owner._id, role_developer._id, role_member._id ]
                # Setup builtin tool applications
                if user_project:
                    p.install_app('profile', 'profile')
                else:
                    p.install_app('home', 'home')
                p.install_app('admin', 'admin')
                p.install_app('search', 'search')
                ThreadLocalORMSession.flush_all()
        except:
            ThreadLocalORMSession.close_all()
            log.exception('Error registering project, attempting to drop %s',
                          database)
            try:
                session(p).impl.bind._conn.drop_database(database)
            except:
                log.exception('Error dropping database %s', database)
                pass
            raise
        g.publish('react', 'forge.project_created')
        return p

    def register_subproject(self, project, name, user, install_apps):
        from allura import model as M
        assert h.re_path_portion.match(name), 'Invalid subproject shortname'
        shortname = project.shortname + '/' + name
        sp = M.Project(
            parent_id=project._id,
            neighborhood_id=project.neighborhood_id,
            shortname=shortname,
            name=name,
            database=project.database,
            last_updated = datetime.utcnow(),
            is_root=False)
        with h.push_config(c, project=sp):
            M.AppConfig.query.remove(dict(project_id=c.project._id))
            if install_apps:
                sp.install_app('home', 'home')
                sp.install_app('admin', 'admin')
                sp.install_app('search', 'search')
            g.publish('react', 'forge.project_created')
        return sp

    def delete_project(self, project, user):
        for sp in project.subprojects:
            self.delete_project(sp, user)
        project.deleted = True

    def undelete_project(self, project, user):
        project.deleted = False
        for sp in project.subprojects:
            self.undelete_project(sp, user)

    def best_download_url(self, project):
        '''This is the url needed to render a download button.
           It should be overridden for your specific envirnoment'''
        return None

class LocalProjectRegistrationProvider(ProjectRegistrationProvider):
    pass
