package org.apereo.cas.authentication;

import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.util.CollectionUtils;
import org.springframework.util.Assert;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

/**
 * Constructs immutable {@link Authentication} objects using the builder pattern.
 *
 * @author Marvin S. Addison
 * @since 4.0.0
 */
public class DefaultAuthenticationBuilder implements AuthenticationBuilder {
    private static final long serialVersionUID = -8504842011648432398L;
    /** Authenticated principal. */
    private Principal principal;

    /** Credential metadata. */
    private List<CredentialMetaData> credentials = new ArrayList<>();

    /** Authentication metadata attributes. */
    private Map<String, Object> attributes = new LinkedHashMap<>();

    /** Map of handler names to authentication successes. */
    private Map<String, HandlerResult> successes = new LinkedHashMap<>();

    /** Map of handler names to authentication failures. */
    private Map<String, Class<? extends Exception>> failures = new LinkedHashMap<>();

    /** Authentication date. */
    private ZonedDateTime authenticationDate;

    /**
     * Creates a new instance using the current date for the authentication date.
     */
    public DefaultAuthenticationBuilder() {
        this.authenticationDate = ZonedDateTime.now();
    }

    /**
     * Creates a new instance using the current date for the authentication date and the given
     * principal for the authenticated principal.
     *
     * @param p Authenticated principal.
     */
    public DefaultAuthenticationBuilder(final Principal p) {
        this();
        this.principal = p;
    }

    /**
     * Gets the authentication date.
     *
     * @return Authentication date.
     */
    public ZonedDateTime getAuthenticationDate() {
        return this.authenticationDate;
    }

    /**
     * Sets the authentication date and returns this instance.
     *
     * @param d Authentication date.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder setAuthenticationDate(final ZonedDateTime d) {
        this.authenticationDate = d;
        return this;
    }

    /**
     * Gets the authenticated principal.
     *
     * @return Principal.
     */
    @Override
    public Principal getPrincipal() {
        return this.principal;
    }

    @Override
    public AuthenticationBuilder addCredentials(final List<CredentialMetaData> credentials) {
        this.credentials.addAll(credentials);
        return this;
    }

    /**
     * Sets the principal returns this instance.
     *
     * @param p Authenticated principal.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder setPrincipal(final Principal p) {
        this.principal = p;
        return this;
    }

    /**
     * Gets the list of credentials that were attempted to be authenticated.
     *
     * @return Non-null list of credentials that attempted authentication.
     */
    public List<CredentialMetaData> getCredentials() {
        return this.credentials;
    }

    /**
     * Sets the list of metadata about credentials presented for authentication.
     *
     * @param credentials Non-null list of credential metadata.
     *
     * @return This builder instance.
     */
    public AuthenticationBuilder setCredentials(final List<CredentialMetaData> credentials) {
        Assert.notNull(credentials, "Credential cannot be null");
        this.credentials.clear();
        this.credentials.addAll(credentials);
        return this;
    }

    /**
     * Adds metadata about a credential presented for authentication.
     *
     * @param credential Credential metadata.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder addCredential(final CredentialMetaData credential) {
        this.credentials.add(credential);
        return this;
    }

    /**
     * Gets the authentication attribute map.
     *
     * @return Non-null authentication attribute map.
     */
    public Map<String, Object> getAttributes() {
        return this.attributes;
    }

    /**
     * Sets the authentication metadata attributes.
     *
     * @param attributes Non-null map of authentication metadata attributes.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder setAttributes(final Map<String, Object> attributes) {
        this.attributes.clear();
        this.attributes.putAll(attributes);
        return this;
    }

    @Override
    public AuthenticationBuilder mergeAttribute(final String key, final Object value) {
        final Object currentValue = this.attributes.get(key);
        if (currentValue == null) {
            return addAttribute(key, value);
        }
        final Collection collection = CollectionUtils.toCollection(currentValue);
        collection.addAll(CollectionUtils.toCollection(value));
        return addAttribute(key, collection);
    }

    @Override
    public boolean hasAttribute(final String name, final Predicate<Object> predicate) {
        if (this.attributes.containsKey(name)) {
            final Object value = this.attributes.get(name);
            final Collection valueCol = CollectionUtils.toCollection(value);
            return valueCol.stream().filter(predicate).count() > 0;
        }
        return false;
    }

    /**
     * Adds an authentication metadata attribute key-value pair.
     *
     * @param key Authentication attribute key.
     * @param value Authentication attribute value.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder addAttribute(final String key, final Object value) {
        this.attributes.put(key, value);
        return this;
    }

    /**
     * Gets the authentication success map.
     *
     * @return Non-null map of handler names to successful handler authentication results.
     */
    @Override
    public Map<String, HandlerResult> getSuccesses() {
        return this.successes;
    }

    /**
     * Sets the authentication handler success map.
     *
     * @param successes Non-null map of handler names to successful handler authentication results.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder setSuccesses(final Map<String, HandlerResult> successes) {
        Assert.notNull(successes, "Successes cannot be null");
        this.successes.clear();
        return addSuccesses(successes);
    }

    @Override
    public AuthenticationBuilder addSuccesses(final Map<String, HandlerResult> successes) {
        successes.entrySet().stream().forEach(entry -> addSuccess(entry.getKey(), entry.getValue()));
        return this;
    }

    /**
     * Adds an authentication success to the map of handler names to successful authentication handler results.
     *
     * @param key Authentication handler name.
     * @param value Successful authentication handler result produced by handler of given name.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder addSuccess(final String key, final HandlerResult value) {
        this.successes.put(key, value);
        return this;
    }

    /**
     * Gets the authentication failure map.
     *
     * @return Non-null authentication failure map.
     */
    @Override
    public Map<String, Class<? extends Exception>> getFailures() {
        return this.failures;
    }

    /**
     * Sets the authentication handler failure map.
     *
     * @param failures Non-null map of handler name to authentication failures.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder setFailures(final Map<String, Class<? extends Exception>> failures) {
        Assert.notNull(failures, "Failures cannot be null");
        this.failures.clear();
        return addFailures(failures);
    }

    @Override
    public AuthenticationBuilder addFailures(final Map<String, Class<? extends Exception>> failures) {
        failures.entrySet().stream().forEach(entry -> addFailure(entry.getKey(), entry.getValue()));
        return this;
    }

    /**
     * Adds an authentication failure to the map of handler names to the authentication handler failures.
     *
     * @param key Authentication handler name.
     * @param value Exception raised on handler failure to authenticate credential.
     *
     * @return This builder instance.
     */
    @Override
    public AuthenticationBuilder addFailure(final String key, final Class<? extends Exception> value) {
        this.failures.put(key, value);
        return this;
    }

    /**
     * Creates an immutable authentication instance from builder data.
     *
     * @return Immutable authentication.
     */
    @Override
    public Authentication build() {
        return new DefaultAuthentication(
                this.authenticationDate,
                this.credentials,
                this.principal,
                this.attributes,
                this.successes,
                this.failures);
    }

    /**
     * Creates a new builder initialized with data from the given authentication source.
     *
     * @param source Authentication source.
     *
     * @return New builder instance initialized with all fields in the given authentication source.
     */
    public static AuthenticationBuilder newInstance(final Authentication source) {
        final DefaultAuthenticationBuilder builder = new DefaultAuthenticationBuilder(source.getPrincipal());
        builder.setAuthenticationDate(source.getAuthenticationDate());
        builder.setCredentials(source.getCredentials());
        builder.setSuccesses(source.getSuccesses());
        builder.setFailures(source.getFailures());
        builder.setAttributes(source.getAttributes());
        return builder;
    }

    /**
     * Creates a new builder.
     *
     * @return New builder instance
     */
    public static AuthenticationBuilder newInstance() {
        return new DefaultAuthenticationBuilder();
    }
}
