package nl.trifork.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.stereotype.Component;

import java.security.Principal;
import java.util.List;

import static java.util.stream.Collectors.toList;
import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;

/**
 * SessionRegistry that retrieves session information from Spring Session, rather than maintaining it by itself.
 * This allows concurrent session management with Spring Security in a clustered environment.
 * <p>
 * Note that expiring a SessionInformation when reaching the configured maximum will simply delete an existing session
 * rather than marking it as expired, since Spring Session has no way to programmatically mark a session as expired.
 * This means that you cannot configure an expired URL; users will simply lose their session as if they logged out.
 * <p>
 * Relies on being able to derive the same String-based representation of the principal given to
 * {@link #getAllSessions(Object, boolean)} as used by Spring Session in order to look up the user's sessions.
 * <p>
 * Does not support {@link #getAllPrincipals()}, since that information is not available.
 */
@Component
public class SpringSessionBackedSessionRegistry implements SessionRegistry {

    private FindByIndexNameSessionRepository<? extends ExpiringSession> sessionRepository;

    @SuppressWarnings("SpringJavaAutowiringInspection")
    @Autowired
    public SpringSessionBackedSessionRegistry(FindByIndexNameSessionRepository<? extends ExpiringSession> sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    @Override
    public List<Object> getAllPrincipals() {
        throw new UnsupportedOperationException("SpringSessionBackedSessionRegistry does not support retrieving all principals, since Spring Session provides no way to obtain that information");
    }

    @Override
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        return sessionRepository
                .findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, name(principal))
                .values()
                .stream()
                .filter(session -> includeExpiredSessions || !session.isExpired())
                .map(session -> new SpringSessionBackedSessionInformation(session, sessionRepository))
                .collect(toList());
    }

    @Override
    public SessionInformation getSessionInformation(String sessionId) {
        ExpiringSession session = sessionRepository.getSession(sessionId);
        if (session != null) {
            return new SpringSessionBackedSessionInformation(session, sessionRepository);
        }
        return null;
    }

    /**
     * This is a no-op, as we don't administer sessions ourselves.
     */
    @Override
    public void refreshLastRequest(String sessionId) {
    }

    /**
     * This is a no-op, as we don't administer sessions ourselves.
     */
    @Override
    public void registerNewSession(String sessionId, Object principal) {
    }

    /**
     * This is a no-op, as we don't administer sessions ourselves.
     */
    @Override
    public void removeSessionInformation(String sessionId) {
    }

    /**
     * Derives a String name for the given principal.
     */
    private String name(Object principal) {
        if (principal instanceof UserDetails) {
            return ((UserDetails) principal).getUsername();
        }
        if (principal instanceof Principal) {
            return ((Principal) principal).getName();
        }
        return principal.toString();
    }
}