package com.ease.gsms.server.util;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/*
* see https://articles.javatalks.ru/articles/25
 */
public class RaceAvoidingPersistentTokenBasedRememberMeServices  extends PersistentTokenBasedRememberMeServices {

    private static final int TOKEN_CACHE_MAX_SIZE = 1000;

    private final Map<String, CachedRememberMeTokenInfo> tokenCache = new ConcurrentHashMap<>();

    private final PersistentTokenRepository tokenRepository;

    private int cachedTokenValidityTime = 30 * 1000;

    public RaceAvoidingPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
        this.tokenRepository = tokenRepository;
    }

    /**
     * Causes a logout to be completed. The method must complete successfully.
     * Removes client's token which is extracted from the HTTP request.
     * {@inheritDoc}
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String cookie = extractRememberMeCookie(request);
        if (cookie != null) {
            String[] seriesAndToken = decodeCookie(cookie);
            super.logout(request, response, authentication);
            tokenCache.remove(seriesAndToken[0]);;
        }
    }

    /**
     * Solution for preventing "remember-me" bug. Some browsers sends preloading requests to server to speed-up
     * page loading. It may cause error when response of preload request not returned to client and second request
     * from client was send. This method implementation stores token in cache for <link>CACHED_TOKEN_VALIDITY_TIME</link>
     * milliseconds and check token presence in cache before process authentication. If there is no equivalent token in
     * cache authentication performs normally. If equivalent present in cache we should not update token in database.
     * This approach can provide acceptable security level and prevent errors.
     * {@inheritDoc}
     * @see <a href="http://jira.jtalks.org/browse/JC-1743">JC-1743</a>
     * @see <a href="https://developers.google.com/chrome/whitepapers/prerender?csw=1">Page preloading in Google Chrome</a>
     */
    @Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain " + 2 +
                    " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        }

        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];

        PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);

        if (token == null) {
            // No series match, so we can't authenticate using this cookie
            throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
        }

        UserDetails details;

        if (isTokenCached(presentedSeries, presentedToken)) {
            details = getUserDetailsService().loadUserByUsername(token.getUsername());
            setCookie(new String[] {token.getSeries(), token.getTokenValue()}, getTokenValiditySeconds(), request, response);
            System.out.println(
                    String.format(
                            "%d - %s - Found a cached token with series: %s, token: %s - setting cookie value to %s - %s",
                            System.currentTimeMillis(), request.getRemoteHost(), presentedSeries, presentedToken, token.getTokenValue(), request.getRequestURI()
                    )
            );
        } else {
            /* IMPORTANT: We should store token in cache before calling <code>loginWithSpringSecurity</code> method.
               Because execution of this method can take a long time.
             */
            System.out.println(
                    String.format(
                            "%d - %s - Haven't found a cached token with series: %s, token: %s - caching token and doing processAutoLoginCookie - %s",
                            System.currentTimeMillis(), request.getRemoteHost(), presentedSeries, presentedToken, request.getRequestURI()
                    )
            );
            cacheToken(token);
            try {
                details = super.processAutoLoginCookie(cookieTokens, request, response);
                //We should remove token from cache if cookie really was stolen or other authentication error occurred
            } catch (RememberMeAuthenticationException ex) {
                System.out.println(
                        String.format(
                                "%d - %s - Cookie theft crap with series: %s, token: %s - removing series from cache - %s",
                                System.currentTimeMillis(), request.getRemoteHost(), presentedSeries, presentedToken, request.getRequestURI()
                        )
                );
                tokenCache.remove(token.getSeries());
                throw ex;
            }
        }
        validateTokenCache();

        return details;
    }

    /**
     * Stores token in cache.
     * @param token Token to be stored
     * @see CachedRememberMeTokenInfo
     */
    private void cacheToken(PersistentRememberMeToken token) {
        if (tokenCache.size() >= TOKEN_CACHE_MAX_SIZE) {
            validateTokenCache();
        }
        CachedRememberMeTokenInfo tokenInfo = new CachedRememberMeTokenInfo(token.getTokenValue(), System.currentTimeMillis());
        tokenCache.put(token.getSeries(), tokenInfo);
    }

    /**
     * Removes from cache tokens which were stored more than <link>CACHED_TOKEN_VALIDITY_TIME</link> milliseconds ago.
     */
    @Scheduled(fixedRate = 30000)
    private void validateTokenCache() {
        for (Map.Entry<String, CachedRememberMeTokenInfo> entry: tokenCache.entrySet()) {
            if (!isTokenInfoValid(entry.getValue())) {
                tokenCache.remove(entry.getKey());
            }
        }
    }

    /**
     * Checks if given tokenInfo valid.
     * @param tokenInfo Token info to be checked
     * @return <code>true</code> tokenInfo was stored in cache less than <link>CACHED_TOKEN_VALIDITY_TIME</link> milliseconds ago.
     * <code>false</code> otherwise.
     * @see CachedRememberMeTokenInfo
     */
    private boolean isTokenInfoValid(CachedRememberMeTokenInfo tokenInfo) {
        if ((System.currentTimeMillis() - tokenInfo.getCachingTime()) >= cachedTokenValidityTime) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Checks if token with given series and value stored in cache
     * @param series series to be checked
     * @param value value to be checked
     * @return <code>true</code> if token stored in cache< <code>false</code> otherwise.
     */
    private boolean isTokenCached(String series, String value) {
        CachedRememberMeTokenInfo tokenInfo = tokenCache.get(series);
        if (tokenInfo != null && isTokenInfoValid(tokenInfo) && value.equals(tokenInfo.getValue())) {
            return true;
        }
        return false;
    }

    /**
     * Needed for possibility to test.
     */
    public void setCachedTokenValidityTime(int cachedTokenValidityTime) {
        this.cachedTokenValidityTime = cachedTokenValidityTime;
    }

/*
    protected UserDetails overridenProcessAutoLoginCookie(String[] cookieTokens,
                                                 HttpServletRequest request, HttpServletResponse response) {

        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain " + 2
                    + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        }

        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];

        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);

        if (token == null) {
            // No series match, so we can't authenticate using this cookie
            throw new RememberMeAuthenticationException(
                    "No persistent token found for series id: " + presentedSeries);
        }

        // We have a match for this user/series combination
        if (!presentedToken.equals(token.getTokenValue())) {
            // Token doesn't match series value. Delete all logins for this user and throw
            // an exception to warn them.
            // tokenRepository.removeUserTokens(token.getUsername()); // vrosca: don't remove the tokens

            throw new CookieTheftException(
                    messages.getMessage(
                            "PersistentTokenBasedRememberMeServices.cookieStolen",
                            "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        }

        if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
                .currentTimeMillis()) {
            throw new RememberMeAuthenticationException("Remember-me login has expired");
        }

        // Token also matches, so login is valid. Update the token value, keeping the
        // *same* series number.
        if (logger.isDebugEnabled()) {
            logger.debug("Refreshing persistent login token for user '"
                    + token.getUsername() + "', series '" + token.getSeries() + "'");
        }

        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());

        try {
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            setCookie(new String[] { token.getSeries(), token.getTokenValue() },
                    getTokenValiditySeconds(), request, response);
        }
        catch (Exception e) {
            logger.error("Failed to update token: ", e);
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }

        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }
*/

}