/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.ki.mgt;

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

import org.apache.ki.authc.AuthenticationException;
import org.apache.ki.authc.AuthenticationInfo;
import org.apache.ki.authc.AuthenticationToken;
import org.apache.ki.authc.RememberMeAuthenticationToken;
import org.apache.ki.codec.Base64;
import org.apache.ki.codec.Hex;
import org.apache.ki.crypto.BlowfishCipher;
import org.apache.ki.crypto.Cipher;
import org.apache.ki.io.DefaultSerializer;
import org.apache.ki.io.SerializationException;
import org.apache.ki.io.Serializer;
import org.apache.ki.subject.PrincipalCollection;


/**
 * Abstract implementation of the <code>RememberMeManager</code> interface that handles
 * {@link #setSerializer(org.apache.ki.io.Serializer) serialization} and
 * {@link #setCipher(org.apache.ki.crypto.Cipher) encryption} of the remembered user identity.
 * <p/>
 * The remembered identity storage location is implementation-specific.
 *
 * @author Les Hazlewood
 * @author Jeremy Haile
 * @since 0.9
 */
public abstract class AbstractRememberMeManager implements RememberMeManager {

    //TODO - complete JavaDoc

    /**
     * private inner log instance.
     */
    private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class);

    private Serializer serializer = new DefaultSerializer();
    private Cipher cipher = new BlowfishCipher();
    private byte[] encryptionCipherKey = null;
    private byte[] decryptionCipherKey = null;

    public AbstractRememberMeManager() {
    }

    public Serializer getSerializer() {
        return serializer;
    }

    public void setSerializer(Serializer serializer) {
        this.serializer = serializer;
    }

    public Cipher getCipher() {
        return cipher;
    }

    public void setCipher(Cipher cipher) {
        this.cipher = cipher;
    }

    public byte[] getEncryptionCipherKey() {
        return encryptionCipherKey;
    }

    public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
        this.encryptionCipherKey = encryptionCipherKey;
    }

    public void setEncryptionCipherKeyHex(String hex) {
        setEncryptionCipherKey(Hex.decode(hex));
    }

    public void setEncryptionCipherKeyBase64(String base64) {
        setEncryptionCipherKey(Base64.decode(base64));
    }

    public byte[] getDecryptionCipherKey() {
        return decryptionCipherKey;
    }

    public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
        this.decryptionCipherKey = decryptionCipherKey;
    }

    public void setDecryptionCipherKeyHex(String hex) {
        setDecryptionCipherKey(Hex.decode(hex));
    }

    public void setDecryptionCipherKeyBase64(String base64) {
        setDecryptionCipherKey(Base64.decode(base64));
    }

    public byte[] getCipherKey() {
        //Since this method should only be used with symmetric ciphers
        //(where the enc and dec keys are the same), either is fine, just return one of them:
        return getEncryptionCipherKey();
    }

    public void setCipherKey(byte[] cipherKey) {
        //Since this method should only be used in symmetric ciphers
        //(where the enc and dec keys are the same), set it on both:
        setEncryptionCipherKey(cipherKey);
        setDecryptionCipherKey(cipherKey);
    }

    public void setCipherKeyHex(String hex) {
        setCipherKey(Hex.decode(hex));
    }

    public void setCipherKeyBase64(String base64) {
        setCipherKey(Base64.decode(base64));
    }

    // Abstract methods to be implemented by subclasses
    protected abstract void rememberSerializedIdentity(byte[] serialized);

    protected abstract byte[] getSerializedRememberedIdentity();

    protected abstract void forgetIdentity();


    protected boolean isRememberMe(AuthenticationToken token) {
        return token != null && (token instanceof RememberMeAuthenticationToken) &&
                ((RememberMeAuthenticationToken) token).isRememberMe();
    }

    public void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info) {
        //always clear any previous identity:
        forgetIdentity(token);

        //reset it if necessary:
        if (isRememberMe(token)) {
            rememberIdentity(token, info);
        } else {
            if (log.isDebugEnabled()) {
                log.debug("AuthenticationToken did not indicate RememberMe is requested.  " +
                        "RememberMe functionality will not be executed for corresponding account.");
            }
        }
    }

    public void rememberIdentity(AuthenticationToken submittedToken, AuthenticationInfo successfullyAuthenticated) {
        rememberIdentity(successfullyAuthenticated);
    }

    public void rememberIdentity(AuthenticationInfo successfullyAuthenticated) {
        PrincipalCollection principals = getIdentityToRemember(successfullyAuthenticated);
        rememberIdentity(principals);
    }

    protected PrincipalCollection getIdentityToRemember(AuthenticationInfo info) {
        return info.getPrincipals();
    }

    protected void rememberIdentity(PrincipalCollection accountPrincipals) {
        try {
            byte[] bytes = serialize(accountPrincipals);
            if (getCipher() != null) {
                bytes = encrypt(bytes);
            }
            rememberSerializedIdentity(bytes);
        } catch (SerializationException se) {
            if (log.isWarnEnabled()) {
                log.warn("Unable to serialize account principals [" + accountPrincipals + "].  Identity " +
                        "cannot be remembered!  This is a non fatal exception as RememberMe identity services " +
                        "are not considered critical and execution can continue as normal.  But please " +
                        "investigate and resolve to prevent seeing this message again.", se);
            }
        }
    }

    public PrincipalCollection getRememberedPrincipals() {
        try {

            PrincipalCollection principals = null;
            byte[] bytes = getSerializedRememberedIdentity();
            if (bytes != null) {
                if (getCipher() != null) {
                    bytes = decrypt(bytes);
                }
                try {
                    principals = deserialize(bytes);
                } catch (SerializationException e) {
                    if (log.isWarnEnabled()) {
                        log.warn("Unable to deserialize stored identity byte array.  Remembered identity " +
                                "cannot be reconstituted!  This is a non fatal exception as RememberMe identity services " +
                                "are not considered critical and execution can continue as normal, but please " +
                                "investigate and resolve to prevent seeing this message again.", e);
                    }
                }
            }
            return principals;

        } catch (Exception e) {
            return onRememberedPrincipalFailure(e);
        }
    }

    /**
     * Called when an exception is thrown while trying to retrieve principals.  The default implementation logs a
     * warning and forgets ('unremembers') the problem identity by calling {@link #forgetIdentity() forgetIdentity()}.
     * This most commonly would occur when an encryption key is updated and old principals are retrieved that have
     * been encrypted with the previous key.\
     *
     * @param e the exception that was thrown.
     * @return <code>null</code> in all cases.
     */
    protected PrincipalCollection onRememberedPrincipalFailure(Exception e) {
        if (log.isWarnEnabled()) {
            log.warn("There was a failure while trying to retrieve remembered principals.  This could be due to a " +
                    "configuration problem or corrupted principals.  This could also be due to a recently " +
                    "changed encryption key.  The remembered identity will be forgotten and not used for this " +
                    "request.", e);
        }
        forgetIdentity();
        return null;
    }

    protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        Cipher cipher = getCipher();
        if (cipher != null) {
            value = cipher.encrypt(serialized, getEncryptionCipherKey());
        }
        return value;
    }

    protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        Cipher cipher = getCipher();
        if (cipher != null) {
            serialized = cipher.decrypt(encrypted, getDecryptionCipherKey());
        }
        return serialized;
    }


    protected byte[] serialize(PrincipalCollection principals) {
        return getSerializer().serialize(principals);
    }

    protected PrincipalCollection deserialize(byte[] serializedIdentity) {
        return (PrincipalCollection) getSerializer().deserialize(serializedIdentity);
    }

    public void onFailedLogin(AuthenticationToken token, AuthenticationException ae) {
        forgetIdentity(token, ae);
    }

    public void onLogout(PrincipalCollection subjectPrincipals) {
        forgetIdentity();
    }

    protected void forgetIdentity(AuthenticationToken token, AuthenticationException ae) {
        forgetIdentity(token);
    }

    protected void forgetIdentity(AuthenticationToken token) {
        forgetIdentity();
    }

}