/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.exoplatform.web.security.security;

import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.management.annotations.Impact;
import org.exoplatform.management.annotations.ImpactType;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.web.login.LoginServlet;
import org.exoplatform.web.security.Token;
import org.exoplatform.web.security.TokenStore;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.common.util.Base64;
import org.gatein.common.util.Base64.EncodingOption;
import org.gatein.wci.security.Credentials;
import org.picocontainer.Startable;

/**
 * Created by The eXo Platform SAS Author : liem.nguyen ncliam@gmail.com Jun 5, 2009
 *
 * todo julien : - make delay configuration from init param and @Managed setter - start/stop expiration daemon - manually invoke
 * the daemon via @Managed
 *
 * @param <T> the token type
 * @param <K> the token key type
 */
@Managed
@ManagedDescription("Token Store Service")
@NameTemplate({ @Property(key = "service", value = "TokenStore"), @Property(key = "name", value = "{Name}") })
public abstract class AbstractTokenService<T extends Token, K> implements Startable, TokenStore {

    protected final Logger log = LoggerFactory.getLogger(getClass());

    protected static final String SERVICE_CONFIG = "service.configuration";

    protected static final String CLEANUP_PERIOD_TIME = "cleanup.period.time";

    /**
     * See {@link #tokenByteLength}. 8 bytes (64 bits) would be enough, but we want to get padding-less Byte64 representation,
     * so we take the next greater number divisible by 3 which is 9. 9 bytes is equal to 72 bits.
     */
    public static final int DEFAULT_TOKEN_BYTE_LENGTH = 9;

    /**
     * The number of random bits generared by {@link #nextRandom()}. Use values divisible by 3 to produce random strings
     * consisting only of {@code 0-9}, {@code a-z}, {@code A-Z}, {@code -} and {@code _}, i.e. URL safe Byte64 without padding.
     * If a value not divisible by 3 is used the random strings will contain {@code *} in addition to the named characters.
     */
    protected final int tokenByteLength;

    protected String name;

    protected long validityMillis;

    protected int delay_time = 600;

    private ScheduledExecutorService executor;

    @SuppressWarnings("unchecked")
    public AbstractTokenService(InitParams initParams) throws TokenServiceInitializationException {
        List<String> params = initParams.getValuesParam(SERVICE_CONFIG).getValues();
        this.name = params.get(0);
        long configValue = new Long(params.get(1));
        this.validityMillis = TimeoutEnum.valueOf(params.get(2)).toMilisecond(configValue);
        this.tokenByteLength = DEFAULT_TOKEN_BYTE_LENGTH;
        ValueParam delayParam = initParams.getValueParam(CLEANUP_PERIOD_TIME);
        if(delayParam != null) {
            delay_time = Integer.parseInt(delayParam.getValue());
        }
    }

    public void start() {
        if(delay_time > 0) {
         // start a thread, garbage expired cookie token every [DELAY_TIME]
            executor = Executors.newSingleThreadScheduledExecutor();
            executor.scheduleWithFixedDelay(new Runnable() {
                public void run() {
                    try {
                        AbstractTokenService.this.cleanExpiredTokens();
                    } catch (Throwable t) {
                        log.warn("Failed to clean expired tokens", t);
                    }
                }
            }, 0, delay_time, TimeUnit.SECONDS);
        }
    }

    public void stop() {
        if(executor != null) {
            executor.shutdown();
        }
    }

    public static <T extends AbstractTokenService<?, ?>> T getInstance(Class<T> classType) {
        PortalContainer container = PortalContainer.getInstance();
        return classType.cast(container.getComponentInstanceOfType(classType));
    }

    public Credentials validateToken(String stringKey, boolean remove) {
        if (stringKey == null) {
            throw new NullPointerException();
        }

        //
        K tokenKey = decodeKey(stringKey);

        T token;
        try {
            if (remove) {
                token = this.deleteToken(tokenKey);
            } else {
                token = this.getToken(tokenKey);
            }

            if (token != null) {
                boolean valid = token.getExpirationTimeMillis() > System.currentTimeMillis();
                if (valid) {
                    return token.getPayload();
                } else if (!remove) {
                    this.deleteToken(tokenKey);
                }
            }
        } catch (Exception e) {
        }

        return null;
    }

    @Managed
    @ManagedDescription("Clean all tokens are expired")
    @Impact(ImpactType.IDEMPOTENT_WRITE)
    public abstract void cleanExpiredTokens();

    @Managed
    @ManagedDescription("Get time for token expiration in seconds")
    public long getValidityTime() {
        return validityMillis / 1000;
    }

    @Managed
    @ManagedDescription("The expiration daemon period time in seconds")
    public long getPeriodTime() {
        return delay_time;
    }

    @Managed
    @ManagedDescription("The token service name")
    public String getName() {
        return name;
    }

    public abstract T getToken(K id);

    public abstract T deleteToken(K id);

    /**
     * Decode a key from its string representation.
     *
     * @param stringKey the key a s a string
     * @return the typed key
     */
    protected abstract K decodeKey(String stringKey);

    // We don't make it a property as retrieving the value can be an expensive operation
    @Managed
    @ManagedDescription("The number of tokens")
    @Impact(ImpactType.READ)
    public abstract long size();

    private enum TimeoutEnum {
        SECOND(1000), MINUTE(1000 * 60), HOUR(1000 * 60 * 60), DAY(1000 * 60 * 60 * 24);

        private long multiply;

        private TimeoutEnum(long multiply) {
            this.multiply = multiply;
        }

        public long toMilisecond(long configValue) {
            return configValue * multiply;
        }
    }

    protected String nextTokenId() {
        return LoginServlet.COOKIE_NAME + nextRandom();
    }

    protected String nextRandom() {
        byte[] randomBytes = new byte[tokenByteLength];
        PortalContainer container = PortalContainer.getInstance();
        SecureRandom random = ((SecureRandomService) container.getComponentInstanceOfType(SecureRandomService.class)).getSecureRandom();
        random.nextBytes(randomBytes);
        return Base64.encodeBytes(randomBytes, EncodingOption.USEURLSAFEENCODING);
    }

}
