/*
 * 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.guacamole.net.auth;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Manager service that temporarily stores a user's authentication status while
 * the authentication flow is underway. Authentication attempts are represented
 * as temporary authentication sessions, allowing authentication attempts to
 * span multiple requests, redirects, etc. Invalid or stale authentication
 * sessions are automatically purged from storage.
 *
 * @param <T>
 *     The type of sessions managed by this session manager.
 */
public abstract class AuthenticationSessionManager<T extends AuthenticationSession> {

    /**
     * Map of authentication session identifiers to their associated
     * {@link AuthenticationSession}.
     */
    private final ConcurrentMap<String, T> sessions = new ConcurrentHashMap<>();

    /**
     * Set of identifiers of all sessions that are in a pending state, meaning
     * that the session was successfully created, but the overall auth result
     * has not yet been determined.
     *
     * Exposed as a ConcurrentMap instead of a Set because there is no
     * ConcurrentSet class offering the required atomic operations.
     */
    private final ConcurrentMap<String, Boolean> pendingSessions = new ConcurrentHashMap<>();

    /**
     * Executor service which runs the periodic cleanup task
     */
    private final ScheduledExecutorService executor =
            Executors.newScheduledThreadPool(1);

    /**
     * Creates a new AuthenticationSessionManager that manages in-progress
     * authentication attempts. Invalid, stale sessions are automatically
     * cleaned up.
     */
    public AuthenticationSessionManager() {
        executor.scheduleAtFixedRate(() -> {

            // Invalidate any stale sessions
            for (Map.Entry<String, T> entry : sessions.entrySet()) {
                if (!entry.getValue().isValid()) 
                    invalidateSession(entry.getKey());
            }

        }, 1, 1, TimeUnit.MINUTES);
    }

    /**
     * Generates a cryptographically-secure value identical in form to the
     * session tokens generated by {@link #defer(org.apache.guacamole.auth.sso.AuthenticationSession)}
     * but invalid. The returned value is indistinguishable from a valid token,
     * but is not a valid token.
     *
     * @return
     *     An invalid token value that is indistinguishable from a valid
     *     token.
     */
    public String generateInvalid() {
        return IdentifierGenerator.generateIdentifier();
    }

    /**
     * Remove the session associated with the given identifier, if any, from the
     * map of sessions, and the set of pending sessions.
     *
     * @param identifier
     *     The identifier of the session to remove, if one exists.
     */
    public void invalidateSession(String identifier) {

        // Do not attempt to remove a null identifier
        if (identifier == null)
            return;

        // Remove from the overall list of sessions
        sessions.remove(identifier);

        // Remove from the set of pending sessions
        pendingSessions.remove(identifier);

    }

    /**
     * Reactivate (remove from pending) the session associated with the given
     * session identifier, if any. After calling this method, any session with
     * the given identifier will be ready to be resumed again.
     * 
     * @param identifier
     *     The identifier of the session to reactivate, if one exists.
     */
    public void reactivateSession(String identifier) {

        // Remove from the set of pending sessions to reactivate
        if (identifier != null)
            pendingSessions.remove(identifier);

    }

    /**
     * Resumes the Guacamole side of the authentication process that was
     * previously deferred through a call to defer(). Once invoked, the
     * provided value ceases to be valid for future calls to resume().
     *
     * @param identifier
     *     The unique string returned by the call to defer(). For convenience,
     *     this value may safely be null.
     *
     * @return
     *     The {@link AuthenticationSession} originally provided when defer()
     *     was invoked, or null if the session is no longer valid or no such
     *     value was returned by defer().
     */
    public T resume(String identifier) {
        if (identifier != null) {

            T session = sessions.get(identifier);

            // Mark the session as pending. NOTE: Unless explicitly removed
            // from pending status via a call to reactivateSession(),
            // the next attempt to resume this session will fail
            if (pendingSessions.putIfAbsent(identifier, true) != null) {

                // If the session was already marked as pending, invalidate it
                invalidateSession(identifier);
                return null;

            }

            if (session != null && session.isValid())
                return session;
        }

        return null;

    }

    /**
     * Defers the Guacamole side of authentication for the user having the
     * given authentication session such that it may be later resumed through a
     * call to resume(). If authentication is never resumed, the session will
     * automatically be cleaned up after it ceases to be valid.
     *
     * This method will automatically generate a new identifier.
     *
     * @param session
     *     The {@link AuthenticationSession} representing the in-progress
     *     authentication attempt.
     *
     * @return
     *     A unique and unpredictable string that may be used to represent the
     *     given session when calling resume().
     */
    public String defer(T session) {
        String identifier = IdentifierGenerator.generateIdentifier();
        sessions.put(identifier, session);
        return identifier;
    }

    /**
     * Defers the Guacamole side of authentication for the user having the
     * given authentication session such that it may be later resumed through a
     * call to resume(). If authentication is never resumed, the session will
     * automatically be cleaned up after it ceases to be valid.
     *
     * This method accepts an externally generated ID, which should be a UUID
     * or similar unique identifier.
     *
     * @param session
     *     The {@link AuthenticationSession} representing the in-progress
     *     authentication attempt.
     *
     * @param identifier
     *     A unique and unpredictable string that may be used to represent the
     *     given session when calling resume().
     */
    public void defer(T session, String identifier) {
        sessions.put(identifier, session);
    }

    /**
     * Shuts down the executor service that periodically removes all invalid
     * authentication sessions. This must be invoked when the auth extension is
     * shut down in order to avoid resource leaks.
     */
    public void shutdown() {
        executor.shutdownNow();
    }

}
