package notclive.security.email.rememberMe;

import notclive.security.email.configuration.EmailSecurityProperties;
import notclive.security.email.principals.EmailSecurityEmailKnownPrincipal;
import notclive.security.email.principals.EmailSecurityUserPrincipal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;

import static java.lang.Long.parseLong;

/**
 * Exposes user's email address in a cookie.
 */
@Component
public class EmailSecurityRememberMeServices extends AbstractRememberMeServices {

    private final Charset utf8 = Charset.forName("UTF-8");

    @Autowired
    private Clock clock;

    /**
     */
    @Autowired
    public EmailSecurityRememberMeServices(EmailSecurityProperties properties) {
        super(properties.getRememberMeSecret(), unusedUserDetailsService());
        setCookieName("remember-me");
    }

    private static InMemoryUserDetailsManager unusedUserDetailsService() {
        return new InMemoryUserDetailsManager(new ArrayList<>());
    }

    @Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens,
                                                 HttpServletRequest request,
                                                 HttpServletResponse response)
            throws RememberMeAuthenticationException, UsernameNotFoundException {
        String emailAddress = cookieTokens[0];
        long expiryTime = parseLong(cookieTokens[1]);
        String signature = cookieTokens[2];
        assertCookieHasNotExpired(expiryTime);
        assertCookieHasValidSignature(emailAddress, expiryTime, signature);
        return new User(emailAddress, "", new ArrayList<>());
    }

    private void assertCookieHasNotExpired(long expiryTime) {
        if (expiryTime < clock.millis()) {
            throw new InvalidCookieException("Cookie has expired (expired on '"
                    + new Date(expiryTime) + "'; current time is '" + ZonedDateTime.now() + "')");
        }
    }

    private void assertCookieHasValidSignature(String emailAddress, long expiryTime, String signature) {
        String expectedSignature = generateSignature(emailAddress, expiryTime);
        if (!signature.equals(expectedSignature)) {
            throw new InvalidCookieException("Cookie contained signature '" + signature
                    + "' but expected '" + expectedSignature + "'");
        }
    }

    private String generateSignature(String emailAddress, long expiryTime) {
        try {
            MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
            byte[] data = (emailAddress + expiryTime + getKey()).getBytes(utf8);
            return new String(Hex.encode(sha256.digest(data)));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("No SHA-256 algorithm available!");
        }
    }

    @Override
    protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {
        return new EmailSecurityUserPrincipal(user.getUsername());
    }

    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        EmailSecurityEmailKnownPrincipal emailKnownPrincipal = getEmailKnownPrincipal();
        return emailKnownPrincipal != null && emailKnownPrincipal.getRememberMe();
    }

    private EmailSecurityEmailKnownPrincipal getEmailKnownPrincipal() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication instanceof EmailSecurityEmailKnownPrincipal
                ? (EmailSecurityEmailKnownPrincipal) authentication
                : null;
    }

    @Override
    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        EmailSecurityEmailKnownPrincipal emailKnownPrincipal = getEmailKnownPrincipal();
        if (emailKnownPrincipal == null) {
            return;
        }
        setCookie(request, response, emailKnownPrincipal);
    }

    private void setCookie(HttpServletRequest request, HttpServletResponse response,
                           EmailSecurityEmailKnownPrincipal emailKnownPrincipal) {
        String emailAddress = (String) emailKnownPrincipal.getPrincipal();
        long expiryTime = calculateExpiryTime();
        String signature = generateSignature(emailAddress, expiryTime);
        setCookie(new String[] {emailAddress, String.valueOf(expiryTime), signature}, getTokenValiditySeconds(), request, response);
    }

    private long calculateExpiryTime() {
        return clock.millis() + getTokenValiditySeconds() * 1000;
    }
}
