package de.ids_mannheim.korap.security.auth;

import com.sun.org.apache.xpath.internal.SourceTree;

import de.ids_mannheim.korap.auditing.AuditRecord;
import de.ids_mannheim.korap.config.Attributes;
import de.ids_mannheim.korap.config.BeansFactory;
import de.ids_mannheim.korap.config.KustvaktConfiguration;
import de.ids_mannheim.korap.config.URIParam;
import de.ids_mannheim.korap.exceptions.*;
import de.ids_mannheim.korap.interfaces.AuthenticationIface;
import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
import de.ids_mannheim.korap.interfaces.EncryptionIface;
import de.ids_mannheim.korap.interfaces.ValidatorIface;
import de.ids_mannheim.korap.interfaces.db.AdminHandlerIface;
import de.ids_mannheim.korap.interfaces.db.AuditingIface;
import de.ids_mannheim.korap.interfaces.db.EntityHandlerIface;
import de.ids_mannheim.korap.interfaces.db.UserDataDbIface;
import de.ids_mannheim.korap.interfaces.defaults.ApacheValidator;
import de.ids_mannheim.korap.user.*;
import de.ids_mannheim.korap.user.User.Location;
import de.ids_mannheim.korap.user.User.CorpusAccess;
import de.ids_mannheim.korap.utils.StringUtils;
import de.ids_mannheim.korap.utils.TimeUtils;
import de.ids_mannheim.korap.security.auth.LdapAuth3;

import javax.ws.rs.core.HttpHeaders;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


// import com.novell.ldap.*; search() funktioniert nicht korrekt, ausgewechselt gegen unboundID's Bibliothek 20.04.17/FB
//Using JAR from unboundID:
import com.unboundid.ldap.sdk.LDAPException;




import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.ws.rs.core.MultivaluedMap;

/**
 * contains the logic to authentication and registration processes.
 * Uses
 * interface implementations (AuthenticationIface) for different
 * databases and handlers
 * 
 * @author hanl
 */
public class KustvaktAuthenticationManager extends AuthenticationManagerIface {

    private static Logger jlog = LoggerFactory
            .getLogger(KustvaktAuthenticationManager.class);
    private EncryptionIface crypto;
    private EntityHandlerIface entHandler;
    private AdminHandlerIface adminHandler;
    private AuditingIface auditing;
    private KustvaktConfiguration config;
    private Collection userdatadaos;
    private LoginCounter counter;
    private ValidatorIface validator;

    public KustvaktAuthenticationManager (EntityHandlerIface userdb, 
    									  AdminHandlerIface admindb,
                                          EncryptionIface crypto,
                                          KustvaktConfiguration config,
                                          AuditingIface auditer,
                                          Collection<UserDataDbIface> userdatadaos) {
        this.entHandler = userdb;
        this.adminHandler = admindb;
        this.config = config;
        this.crypto = crypto;
        this.auditing = auditer;
        this.counter = new LoginCounter(config);
        this.userdatadaos = userdatadaos;
        // todo: load via beancontext
        try {
            this.validator = new ApacheValidator();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * get session object if token was a session token
     * 
     * @param token
     * @param host
     * @param useragent
     * @return
     * @throws KustvaktException
     */
    @Override
    public TokenContext getTokenStatus (String token, String host,
            String useragent) throws KustvaktException {
        if (token == null)
            throw new KustvaktException(StatusCodes.MISSING_ARGUMENT, "authorization header");

        String token_type = StringUtils.getTokenType(token);
        token = StringUtils.stripTokenType(token);
        jlog.info("getting session status of token type '{}'",
                token.split(" ")[0]);
        AuthenticationIface provider = getProvider(
                token_type, null);

        if (provider == null)
            // throw exception for missing type parameter
            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
                    "token type not defined or found", "token_type");

        TokenContext context = provider.getTokenContext(token);
        if (context != null && TimeUtils.isExpired(context.getExpirationTime()))
            throw new KustvaktException(StatusCodes.EXPIRED);
        else if (context == null)
            throw new KustvaktException(StatusCodes.NO_VALUE_FOUND);

        //        if (!matchStatus(host, useragent, context))
        //            provider.removeUserSession(token);
        return context;
    }


    @Override
    public User getUser (String username) throws KustvaktException {
        //User user;
        //Object value = this.getCacheValue(username);

        if (User.UserFactory.isDemo(username))
            return User.UserFactory.getDemoUser();

        //if (value != null) {
         //   Map map = (Map) value;
          //  user = User.UserFactory.toUser(map);
        //}
       // else {
        //    user = entHandler.getAccount(username);
        //    this.storeInCache(username, user.toCache());
            // todo: not valid. for the duration of the session, the host should not change!
        //}
        //todo:
        //        user.addField(Attributes.HOST, context.getHostAddress());
        //        user.addField(Attributes.USER_AGENT, context.getUserAgent());
        return entHandler.getAccount(username);
    }



    public TokenContext refresh (TokenContext context) throws KustvaktException {
        AuthenticationIface provider = getProvider(context.getTokenType(), null);
        if (provider == null) {
            //todo:
        }

        try {
            provider.removeUserSession(context.getToken());
            User user = getUser(context.getUsername());
            return provider.createTokenContext(user, context.params());
        }
        catch (KustvaktException e) {
            throw new WrappedException(e, StatusCodes.LOGIN_FAILED);
        }
    }


    /**
     * @param type
     * @param attributes
     *            contains username and password to authenticate the
     *            user.
     *            Depending of the authentication schema, may contain
     *            other values as well
     * @return User
     * @throws KustvaktException
     */
    @Override
    public User authenticate (int type, String username, String password,
            Map<String, Object> attributes) throws KustvaktException {
        User user;
        switch (type) {
            case 1:
                // todo:
                user = authenticateShib(attributes);
                break;
            case 2:
                // IdM/LDAP: (09.02.17/FB)
                user = authenticateIdM(username, password, attributes);
                break;
            default:
                user = authenticate(username, password, attributes);
                break;
        }
        auditing.audit(AuditRecord.serviceRecord(user.getId(),
                StatusCodes.LOGIN_SUCCESSFUL, user.toString()));
        return user;
    }

    // a. set location depending on X-Forwarded-For.
    // X-Forwarded-For: clientIP, ProxyID, ProxyID...
    // the following private address spaces may be used to define intranet spaces:
    // 10.0.0.0        -   10.255.255.255  (10/8 prefix)
    // 172.16.0.0      -   172.31.255.255  (172.16/12 prefix)
    // 192.168.0.0     -   192.168.255.255 (192.168/16 prefix)
    // b. set corpusAccess depending on location:
    // 16.05.17/FB
    
    @Override
    public void setAccessAndLocation(User user, HttpHeaders headers)
    
    {
    Boolean DEBUG_LOG = true;
    MultivaluedMap<String,String> headerMap = headers.getRequestHeaders();
    Location location = Location.INTERN;
    CorpusAccess corpusAccess = CorpusAccess.FREE;
    
    if( headerMap != null && headerMap.size() > 0 )
    	{
    	Iterator<String> it = headerMap.keySet().iterator();
    	while( it.hasNext() )
    		{
    		String key = (String)it.next();
    		if( key.equals("X-Forwarded-For"))
        		{
        		List<String> vals = new ArrayList<String>(Arrays.asList(headerMap.getFirst(key).split(",")));
        		String clientAddress = vals.get(0);
        		
        		if( clientAddress.startsWith("10.0.") || clientAddress.startsWith("172.16.") || clientAddress.startsWith("192.168."))
        			location = Location.INTERN;
        		else
        			location = Location.EXTERN;
        		if( location == Location.EXTERN )
        			corpusAccess = CorpusAccess.PUB;
        		else
        			corpusAccess = CorpusAccess.ALL;
        		
        		if( DEBUG_LOG == true )
        			{
	        		System.out.printf("Debug: X-Forwarded-For : '%s' (%d values) -> %s\n", vals, vals.size(), vals.get(0));
	        		System.out.printf("Debug: X-Forwarded-For : location = %s corpusAccess = %s\n", 
	        				location == Location.INTERN ? "INTERN" : "EXTERN",
	        				corpusAccess == CorpusAccess.ALL ? "ALL" : corpusAccess == CorpusAccess.PUB ? "PUB" : "FREE"); 
	        		}
        		}	
    		}
    	}
    
    user.setLocation(location);
    user.setCorpusAccess(corpusAccess);
        
    } // getAccess
    
    @Override
    public TokenContext createTokenContext (User user,
            Map<String, Object> attr, String provider_key)
            throws KustvaktException {
        AuthenticationIface provider = getProvider(provider_key,
                Attributes.API_AUTHENTICATION);

        if (attr.get(Attributes.SCOPES) != null)
            this.getUserData(user, UserDetails.class);

        TokenContext context = provider.createTokenContext(user, attr);
        if (context == null)
            throw new KustvaktException(StatusCodes.NOT_SUPPORTED);
        context.setUserAgent((String) attr.get(Attributes.USER_AGENT));
        context.setHostAddress(Attributes.HOST);
        return context;
    }


    //todo: test
    @Deprecated
    private boolean matchStatus (String host, String useragent,
            TokenContext context) {
        if (host.equals(context.getHostAddress())) {
            if (useragent.equals(context.getUserAgent()))
                return true;
        }
        return false;
    }


    private User authenticateShib (Map<String, Object> attributes)
            throws KustvaktException {
        // todo use persistent id, since eppn is not unique
        String eppn = (String) attributes.get(Attributes.EPPN);

        if (eppn == null || eppn.isEmpty())
            throw new KustvaktException(StatusCodes.REQUEST_INVALID);

        if (!attributes.containsKey(Attributes.EMAIL)
                && validator.isValid(eppn, Attributes.EMAIL))
            attributes.put(Attributes.EMAIL, eppn);

        User user = null;
        if (isRegistered(eppn))
            user = createShibbUserAccount(attributes);
        return user;
    }


    //todo: what if attributes null?
    private User authenticate (String username, String password, Map<String, Object> attr) throws KustvaktException {
    	
        Map<String, Object> attributes = validator.validateMap(attr);
        User unknown;
        // just to make sure that the plain password does not appear anywhere in the logs!

        try {
            validator.validateEntry(username, Attributes.USERNAME);
        } catch (KustvaktException e) {
            throw new WrappedException(e, StatusCodes.LOGIN_FAILED, username);
        }

        if (username == null || username.isEmpty())
            throw new WrappedException(new KustvaktException(username,
                    StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED);
        else {
            try {
                unknown = entHandler.getAccount(username);
            } 
            catch (EmptyResultException e) {
                // mask exception to disable user guessing in possible attacks
                throw new WrappedException(new KustvaktException(username,
                        StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
                        username);
            }
            catch (KustvaktException e) {
                jlog.error("Error: {}", e);
                throw new WrappedException(e, StatusCodes.LOGIN_FAILED,
                        attributes.toString());
            }
        }
        
        boolean isAdmin = adminHandler.isAdmin(unknown.getId());
        unknown.setAdmin(isAdmin);
        jlog.trace("Authentication: found username " + unknown.getUsername());
        
        if (unknown instanceof KorAPUser) {
            if (password == null || password.isEmpty())
                throw new WrappedException(new KustvaktException(
                        unknown.getId(), StatusCodes.BAD_CREDENTIALS),
                        StatusCodes.LOGIN_FAILED, username);

            KorAPUser user = (KorAPUser) unknown;
            boolean check = crypto.checkHash(password, user.getPassword());

            if (!check) {
                // the fail counter only applies for wrong password
                jlog.warn("Wrong Password!");
                processLoginFail(unknown);
                throw new WrappedException(new KustvaktException(user.getId(),
                        StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
                        username);
            }

            // bad credentials error has precedence over account locked or unconfirmed codes
            // since latter can lead to account guessing of third parties
            if (user.isAccountLocked()) {
                URIParam param = (URIParam) user.getField(URIParam.class);

                if (param.hasValues()) {
                    jlog.debug("Account is not yet activated for user '{}'",
                            user.getUsername());
                    if (TimeUtils.getNow().isAfter(param.getUriExpiration())) {
                        jlog.error(
                                "URI token is expired. Deleting account for user {}",
                                user.getUsername());
                        deleteAccount(user);
                        throw new WrappedException(new KustvaktException(
                                unknown.getId(), StatusCodes.EXPIRED,
                                "account confirmation uri has expired!",
                                param.getUriFragment()),
                                StatusCodes.LOGIN_FAILED, username);
                    }
                    throw new WrappedException(new KustvaktException(
                            unknown.getId(), StatusCodes.ACCOUNT_NOT_CONFIRMED),
                            StatusCodes.LOGIN_FAILED, username);
                }
                jlog.error("ACCESS DENIED: account not active for '{}'",
                        unknown.getUsername());
                throw new WrappedException(new KustvaktException(
                        unknown.getId(), StatusCodes.ACCOUNT_DEACTIVATED),
                        StatusCodes.LOGIN_FAILED, username);
            }

        }
        else if (unknown instanceof ShibUser) {
            //todo
        }
        jlog.debug("Authentication done: " + username);
        return unknown;
    }


    /**
     * authenticate using IdM (Identitätsmanagement) accessed by LDAP.
     * @param username
     * @param password
     * @param attr
     * @return
     * @throws KustvaktException
     * @date 09.02.17/FB
     */
    //todo: what if attributes null?
    
    private User authenticateIdM (String username, String password, Map<String, Object> attr) throws KustvaktException {
    	
        Map<String, Object> attributes = validator.validateMap(attr);
        User unknown = null;
        // just to make sure that the plain password does not appear anywhere in the logs!

        System.out.printf("Debug: authenticateIdM: entering for '%s'...\n", username);
        
        /** wozu Apache Validatoren für User/Passwort für IdM/LDAP?
         * siehe validation.properties. Abgeschaltet 21.04.17/FB
        try { 
            validator.validateEntry(username, Attributes.USERNAME);
        	} 
        catch (KustvaktException e) {
            throw new WrappedException(e, StatusCodes.LOGIN_FAILED, username);
        }
		*/
        if (username == null || username.isEmpty() || password == null || password.isEmpty() )
            throw new WrappedException(new KustvaktException(username,
                    StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED);

        // LDAP Access:
        try {
        	// todo: unknown = ...
        	int ret = LdapAuth3.login(username, password);
        	System.out.printf("Debug: autenticationIdM: Ldap.login(%s) returns: %d.\n", username, ret); 
        	if( ret != LdapAuth3.LDAP_AUTH_ROK )
        		{
        		jlog.error("LdapAuth3.login(username='{}') returns '{}'='{}'!", username, ret,
        				LdapAuth3.getErrMessage(ret));
        		
               	// mask exception to disable user guessing in possible attacks
        		/* by Hanl
                throw new WrappedException(new KustvaktException(username,
                            StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
                            username);        		 
        		 */
                throw new WrappedException(
                			new KustvaktException(
                					username,
                					StatusCodes.LDAP_BASE_ERRCODE+ret,
                					LdapAuth3.getErrMessage(ret),
                					null), 
        					StatusCodes.LOGIN_FAILED,
                            username);
        		}
            }
        catch ( LDAPException e ) {
        	
        	jlog.error("Error: username='{}' -> '{}'!", username, e);
        	// mask exception to disable user guessing in possible attacks
        	/* by Hanl:
            throw new WrappedException(new KustvaktException(username,
                        StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
                        username);
            */
            throw new WrappedException(
            		new KustvaktException(
            				username,
            				StatusCodes.LDAP_BASE_ERRCODE+LdapAuth3.LDAP_AUTH_RINTERR,
            				LdapAuth3.getErrMessage(LdapAuth3.LDAP_AUTH_RINTERR),
            				null), 
    				StatusCodes.LOGIN_FAILED,
                    username);
            }

        // Create a User
        // TODO: KorAPUser für solche mit einem bestehenden Account
        //       DefaultUser sonst.
        User user = new KorAPUser();
        user.setUsername(username);
        /* folgender Code funktioniert hier noch nicht, da die Headers noch nicht ausgewertet
         * worden sind - 23.05.17/FB
        Object o = attr.get(Attributes.LOCATION);
        String loc = (String)o.toString();
        int location = Integer.parseInt(loc);
        user.setLocation(location); 
        user.setCorpusAccess(Integer.parseInt(attr.get(Attributes.CORPUS_ACCESS).toString()));
         */
        unknown = user;
        
        jlog.trace("Authentication: found username " + unknown.getUsername());
        
        if (unknown instanceof KorAPUser) {
            /* password already checked using LDAP:
            if (password == null || password.isEmpty())
                throw new WrappedException(new KustvaktException(
                        unknown.getId(), StatusCodes.BAD_CREDENTIALS),
                        StatusCodes.LOGIN_FAILED, username);

            KorAPUser user = (KorAPUser) unknown;
            boolean check = crypto.checkHash(password, user.getPassword());

            if (!check) {
                // the fail counter only applies for wrong password
                jlog.warn("Wrong Password!");
                processLoginFail(unknown);
                throw new WrappedException(new KustvaktException(user.getId(),
                        StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
                        username);
            }
            */
            // bad credentials error has precedence over account locked or unconfirmed codes
            // since latter can lead to account guessing of third parties
            /*
            if (user.isAccountLocked()) {
            
                URIParam param = (URIParam) user.getField(URIParam.class);

                if (param.hasValues()) {
                    jlog.debug("Account is not yet activated for user '{}'",
                            user.getUsername());
                    if (TimeUtils.getNow().isAfter(param.getUriExpiration())) {
                        jlog.error(
                                "URI token is expired. Deleting account for user {}",
                                user.getUsername());
                        deleteAccount(user);
                        throw new WrappedException(new KustvaktException(
                                unknown.getId(), StatusCodes.EXPIRED,
                                "account confirmation uri has expired!",
                                param.getUriFragment()),
                                StatusCodes.LOGIN_FAILED, username);
                    }
                    throw new WrappedException(new KustvaktException(
                            unknown.getId(), StatusCodes.ACCOUNT_NOT_CONFIRMED),
                            StatusCodes.LOGIN_FAILED, username);
                }
                jlog.error("ACCESS DENIED: account not active for '{}'",
                        unknown.getUsername());
                throw new WrappedException(new KustvaktException(
                        unknown.getId(), StatusCodes.ACCOUNT_DEACTIVATED),
                        StatusCodes.LOGIN_FAILED, username);
            }
             */

        }
        else if (unknown instanceof ShibUser) {
            //todo
        }
        
        jlog.debug("Authentication done: " + username);
        return unknown;
        
    } // authenticateIdM

    public boolean isRegistered (String username) {
        User user;
        if (username == null || username.isEmpty())
            return false;
        //    throw new KustvaktException(username, StatusCodes.ILLEGAL_ARGUMENT,
        //            "username must be set", username);

        try {
            user = entHandler.getAccount(username);
        }
        catch (EmptyResultException e) {
            jlog.debug("user does not exist ({})", username);
            return false;

        }
        catch (KustvaktException e) {
            jlog.error("KorAPException", e.string());
            return false;
            //throw new KustvaktException(username, StatusCodes.ILLEGAL_ARGUMENT,
             //       "username invalid", username);
        }
        return user != null;
    }


    public void logout (TokenContext context) throws KustvaktException {
        try {
            AuthenticationIface provider = getProvider(context.getTokenType(),
                    null);

            if (provider == null) {
                throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
                        "provider not supported!", context.getTokenType());
            }
            provider.removeUserSession(context.getToken());
        }
        catch (KustvaktException e) {
            throw new WrappedException(e, StatusCodes.LOGOUT_FAILED,
                    context.toString());
        }
        auditing.audit(AuditRecord.serviceRecord(context.getUsername(),
                StatusCodes.LOGOUT_SUCCESSFUL, context.toString()));
        this.removeCacheEntry(context.getToken());
    }


    private void processLoginFail (User user) throws KustvaktException {
        counter.registerFail(user.getUsername());
        if (!counter.validate(user.getUsername())) {
            try {
                this.lockAccount(user);
            }
            catch (KustvaktException e) {
                jlog.error("user account could not be locked", e);
                throw new WrappedException(e, StatusCodes.UPDATE_ACCOUNT_FAILED);
            }
            throw new WrappedException(new KustvaktException(user.getId(),
                    StatusCodes.ACCOUNT_DEACTIVATED), StatusCodes.LOGIN_FAILED);
        }
    }


    public void lockAccount (User user) throws KustvaktException {
        if (!(user instanceof KorAPUser))
            throw new KustvaktException(StatusCodes.REQUEST_INVALID);

        KorAPUser u = (KorAPUser) user;
        u.setAccountLocked(true);
        jlog.info("locking account for user: {}", user.getUsername());
        entHandler.updateAccount(u);
    }


    public KorAPUser checkPasswordAllowance (KorAPUser user,
            String oldPassword, String newPassword) throws KustvaktException {
        String dbPassword = user.getPassword();

        if (oldPassword.trim().equals(newPassword.trim())) {
            // TODO: special error StatusCodes for this?
            throw new WrappedException(new KustvaktException(user.getId(),
                    StatusCodes.ILLEGAL_ARGUMENT),
                    StatusCodes.PASSWORD_RESET_FAILED, newPassword);
        }

        boolean check = crypto.checkHash(oldPassword, dbPassword);

        if (!check)
            throw new WrappedException(new KustvaktException(user.getId(),
                    StatusCodes.BAD_CREDENTIALS),
                    StatusCodes.PASSWORD_RESET_FAILED);

        try {
            user.setPassword(crypto.secureHash(newPassword));
        }
        catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            throw new WrappedException(new KustvaktException(user.getId(),
                    StatusCodes.ILLEGAL_ARGUMENT, "password invalid",
                    newPassword), StatusCodes.PASSWORD_RESET_FAILED,
                    user.toString(), newPassword);
        }
        return user;
    }


    //fixme: use clientinfo for logging/auditing?! = from where did he access the reset function?
    @Override
    public void resetPassword (String uriFragment, String username,
            String newPassphrase) throws KustvaktException {
        try {
            validator.validateEntry(username, Attributes.USERNAME);
            validator.validateEntry(newPassphrase, Attributes.PASSWORD);
        } catch (KustvaktException e) {
            jlog.error("Error: {}", e.string());
            throw new WrappedException(new KustvaktException(username,
                    StatusCodes.ILLEGAL_ARGUMENT, "password invalid",
                    newPassphrase), StatusCodes.PASSWORD_RESET_FAILED,
                    username, newPassphrase);
        }

        try {
            newPassphrase= crypto.secureHash(newPassphrase);
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            jlog.error("Encoding/Algorithm Error", e);
            throw new WrappedException(new KustvaktException(username,
                    StatusCodes.ILLEGAL_ARGUMENT, "password invalid",
                    newPassphrase), StatusCodes.PASSWORD_RESET_FAILED,
                    username, uriFragment, newPassphrase);
        }
        int result = entHandler
                .resetPassphrase(username, uriFragment, newPassphrase);

        if (result == 0)
            throw new WrappedException(new KustvaktException(username,
                    StatusCodes.EXPIRED, "URI fragment expired", uriFragment),
                    StatusCodes.PASSWORD_RESET_FAILED, username, uriFragment);
        else if (result == 1)
            jlog.info("successfully reset password for user {}", username);
    }


    public void confirmRegistration (String uriFragment, String username)
            throws KustvaktException {
        try {
            validator.validateEntry(username, Attributes.USERNAME);
        } catch (KustvaktException e) {
            jlog.error("Error: {}", e.string());
            throw new WrappedException(e,
                    StatusCodes.ACCOUNT_CONFIRMATION_FAILED, username,
                    uriFragment);
        }
        int r = entHandler.activateAccount(username, uriFragment);
        if (r == 0) {
            User user;
            try {
                user = entHandler.getAccount(username);
            }
            catch (EmptyResultException e) {
                throw new WrappedException(new KustvaktException(username,
                        StatusCodes.BAD_CREDENTIALS),
                        StatusCodes.ACCOUNT_CONFIRMATION_FAILED, username,
                        uriFragment);
            }
            entHandler.deleteAccount(user.getId());
            throw new WrappedException(new KustvaktException(user.getId(),
                    StatusCodes.EXPIRED),
                    StatusCodes.ACCOUNT_CONFIRMATION_FAILED, username,
                    uriFragment);
        }
        else if (r == 1)
            jlog.info("successfully confirmed user registration for user {}",
                    username);
        // register successful audit!
    }


    /**
     * @param attributes
     * @return
     * @throws KustvaktException
     */
    //fixme: remove clientinfo object (not needed), use json representation to get stuff
    public User createUserAccount (Map<String, Object> attributes,
            boolean confirmation_required) throws KustvaktException {
        Map<String, Object> safeMap = validator.validateMap(attributes);

        if (safeMap.get(Attributes.USERNAME) == null
                || ((String) safeMap.get(Attributes.USERNAME)).isEmpty())
            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
                    "username must be set", "username");
        if (safeMap.get(Attributes.PASSWORD) == null
                || ((String) safeMap.get(Attributes.PASSWORD)).isEmpty())
            throw new KustvaktException(safeMap.get(Attributes.USERNAME),
                    StatusCodes.ILLEGAL_ARGUMENT, "password must be set",
                    "password");

        String username = validator.validateEntry(
                (String) safeMap.get(Attributes.USERNAME), Attributes.USERNAME);
        String password = validator.validateEntry(
                (String) safeMap.get(Attributes.PASSWORD), Attributes.PASSWORD);
        String hash;
        try {
            hash = crypto.secureHash(password);
        }
        catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
            jlog.error("Encryption error", e);
            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
        }

        KorAPUser user = User.UserFactory.getUser(username);
        Object id = attributes.get(Attributes.ID);
        if (id != null && id instanceof Integer)
            user.setId((Integer) id);

        user.setAccountLocked(confirmation_required);
        if (confirmation_required) {
            URIParam param = new URIParam(crypto.createToken(), TimeUtils
                    .plusSeconds(config.getTokenTTL()).getMillis());
            user.addField(param);
        }
        user.setPassword(hash);
        
        String o = (String) attributes.get(Attributes.IS_ADMIN);
		boolean b = Boolean.parseBoolean(o);
		user.setAdmin(b);
        
        try {
            UserDetails details = new UserDetails();
            details.read(safeMap, true);

            UserSettings settings = new UserSettings();
            settings.read(safeMap, true);

            jlog.info("Creating new user account for user {}",
                    user.getUsername());
            entHandler.createAccount(user);
            if (user.isAdmin() && user instanceof KorAPUser){
            	adminHandler.addAccount(user);
            }
            details.setUserId(user.getId());
            settings.setUserId(user.getId());

            UserDataDbIface dao = BeansFactory.getTypeFactory()
                    .getTypeInterfaceBean(userdatadaos, UserDetails.class);
            //todo: remove this
            assert dao != null;
            dao.store(details);
            dao = BeansFactory.getTypeFactory().getTypeInterfaceBean(
                    userdatadaos, UserSettings.class);
            assert dao != null;
            dao.store(settings);
        }
        catch (KustvaktException e) {
            jlog.error("Error: {}", e.string());
            throw new WrappedException(e, StatusCodes.CREATE_ACCOUNT_FAILED,
                    user.toString());
        }

        auditing.audit(AuditRecord.serviceRecord(user.getUsername(),
                StatusCodes.CREATE_ACCOUNT_SUCCESSFUL));
        return user;
    }


    //todo:
    private ShibUser createShibbUserAccount (Map<String, Object> attributes)
            throws KustvaktException {
        jlog.debug("creating shibboleth user account for user attr: {}",
                attributes);
        Map<String, Object> safeMap = validator.validateMap(attributes);

        //todo eppn non-unique.join with idp or use persistent_id as username identifier
        ShibUser user = User.UserFactory.getShibInstance(
                (String) safeMap.get(Attributes.EPPN),
                (String) safeMap.get(Attributes.MAIL),
                (String) safeMap.get(Attributes.CN));
        user.setAffiliation((String) safeMap.get(Attributes.EDU_AFFIL));
        user.setAccountCreation(TimeUtils.getNow().getMillis());


        UserDetails d = new UserDetails();
        d.read(attributes, true);

        UserSettings s = new UserSettings();
        s.read(attributes, true);

        entHandler.createAccount(user);

        s.setUserId(user.getId());
        d.setUserId(user.getId());

        UserDataDbIface dao = BeansFactory.getTypeFactory()
                .getTypeInterfaceBean(userdatadaos, UserDetails.class);
        assert dao != null;
        dao.store(d);

        dao = BeansFactory.getTypeFactory().getTypeInterfaceBean(userdatadaos,
                UserSettings.class);
        assert dao != null;
        dao.store(d);

        return user;
    }


    /**
     * link shibboleth and korap user account to one another.
     * 
     * @param current
     *            currently logged in user
     * @param for_name
     *            foreign user name the current account should be
     *            linked to
     * @param transstrat
     *            transfer status of user data (details, settings,
     *            user queries)
     *            0 = the currently logged in data should be kept
     *            1 = the foreign account data should be kept
     * @throws NotAuthorizedException
     * @throws KustvaktException
     */
    // todo:
    public void accountLink (User current, String for_name, int transstrat)
            throws KustvaktException {
        //        User foreign = entHandler.getAccount(for_name);

        //        if (current.getAccountLink() == null && current.getAccountLink()
        //                .isEmpty()) {
        //            if (current instanceof KorAPUser && foreign instanceof ShibUser) {
        //                if (transstrat == 1)
        //                    current.transfer(foreign);
        ////                foreign.setAccountLink(current.getUsername());
        ////                current.setAccountLink(foreign.getUsername());
        //                //                entHandler.purgeDetails(foreign);
        //                //                entHandler.purgeSettings(foreign);
        //            }else if (foreign instanceof KorAPUser
        //                    && current instanceof ShibUser) {
        //                if (transstrat == 0)
        //                    foreign.transfer(current);
        ////                current.setAccountLink(foreign.getUsername());
        //                //                entHandler.purgeDetails(current);
        //                //                entHandler.purgeSettings(current);
        //                //                entHandler.purgeSettings(current);
        //            }
        //        entHandler.updateAccount(current);
        //        entHandler.updateAccount(foreign);
        //        }
    }


    // todo: test and rest usage?!
    public boolean updateAccount (User user) throws KustvaktException {
        boolean result;
        if (user instanceof DemoUser)
            throw new KustvaktException(user.getId(),
                    StatusCodes.REQUEST_INVALID,
                    "account not updateable for demo user", user.getUsername());
        else {
            //            crypto.validate(user);
            try {
                result = entHandler.updateAccount(user) > 0;
            }
            catch (KustvaktException e) {
                jlog.error("Error: {}", e.string());
                throw new WrappedException(e, StatusCodes.UPDATE_ACCOUNT_FAILED);
            }
        }
        if (result) {
            // this.removeCacheEntry(user.getUsername());
            auditing.audit(AuditRecord.serviceRecord(user.getId(),
                    StatusCodes.UPDATE_ACCOUNT_SUCCESSFUL, user.toString()));
        }
        return result;
    }


    public boolean deleteAccount (User user) throws KustvaktException {
        boolean result;
        if (user instanceof DemoUser)
            return true;
        else {
            try {
                result = entHandler.deleteAccount(user.getId()) > 0;
            }
            catch (KustvaktException e) {
                jlog.error("Error: {}", e.string());
                throw new WrappedException(e, StatusCodes.DELETE_ACCOUNT_FAILED);
            }
        }
        if (result) {
            // this.removeCacheEntry(user.getUsername());
            auditing.audit(AuditRecord.serviceRecord(user.getUsername(),
                    StatusCodes.DELETE_ACCOUNT_SUCCESSFUL, user.toString()));
        }
        return result;
    }


    public Object[] validateResetPasswordRequest (String username, String email)
            throws KustvaktException {
        String uritoken;
        validator.validateEntry(email, Attributes.EMAIL);
        User ident;
        try {
            ident = entHandler.getAccount(username);
            if (ident instanceof DemoUser)
                //            throw new NotAuthorizedException(StatusCodes.PERMISSION_DENIED,
                //                    "password reset now allowed for DemoUser", "");
                throw new WrappedException(username,
                        StatusCodes.PASSWORD_RESET_FAILED, username);
        }
        catch (EmptyResultException e) {
            throw new WrappedException(new KustvaktException(username,
                    StatusCodes.ILLEGAL_ARGUMENT, "username not found",
                    username), StatusCodes.PASSWORD_RESET_FAILED, username);
        }

        Userdata data = this.getUserData(ident, UserDetails.class);
        KorAPUser user = (KorAPUser) ident;

        if (!email.equals(data.get(Attributes.EMAIL)))
            //            throw new NotAuthorizedException(StatusCodes.ILLEGAL_ARGUMENT,
            //                    "invalid parameter: email", "email");
            throw new WrappedException(new KustvaktException(user.getId(),
                    StatusCodes.ILLEGAL_ARGUMENT, "email invalid", email),
                    StatusCodes.PASSWORD_RESET_FAILED, email);
        uritoken = crypto.encodeBase();
        URIParam param = new URIParam(uritoken, TimeUtils.plusHours(24)
                .getMillis());
        user.addField(param);

        try {
            entHandler.updateAccount(user);
        }
        catch (KustvaktException e) {
            jlog.error("Error ", e.string());
            throw new WrappedException(e, StatusCodes.PASSWORD_RESET_FAILED);
        }
        return new Object[] { uritoken, TimeUtils.format(param.getUriExpiration()) };
    }


    @Override
    public <T extends Userdata> T getUserData (User user, Class<T> clazz)
            throws WrappedException {

        try {
            UserDataDbIface<T> dao = BeansFactory.getTypeFactory()
                    .getTypeInterfaceBean(
                            BeansFactory.getKustvaktContext()
                                    .getUserDataProviders(), clazz);
            T data = null;
            if (dao != null)
                data = dao.get(user);

            if (data == null)
                throw new KustvaktException(user.getId(),
                        StatusCodes.NO_VALUE_FOUND, "No data found!",
                        clazz.getSimpleName());
            return data;
        }
        catch (KustvaktException e) {
            jlog.error("Error during user data retrieval: {}", e.getEntity());
            throw new WrappedException(e, StatusCodes.GET_ACCOUNT_FAILED);
        }
    }


    //todo: cache userdata outside of the user object!
    @Override
    public void updateUserData (Userdata data) throws WrappedException {
        try {
            data.validate(this.validator);
            UserDataDbIface dao = BeansFactory.getTypeFactory()
                    .getTypeInterfaceBean(
                            BeansFactory.getKustvaktContext()
                                    .getUserDataProviders(), data.getClass());
            if (dao != null)
                dao.update(data);
        }
        catch (KustvaktException e) {
            jlog.error("Error during update of user data!", e.getEntity());
            throw new WrappedException(e, StatusCodes.UPDATE_ACCOUNT_FAILED);
        }
    }
}
