package org.apereo.cas.support.spnego.authentication.principal;

import com.google.common.io.ByteSource;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.principal.Principal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.stream.IntStream;

/**
 * Credential that are a holder for SPNEGO init token.
 *
 * @author Arnaud Lesueur
 * @author Marc-Antoine Garrigue
 * @since 3.1
 */
public class SpnegoCredential implements Credential, Serializable {

    /**
     * Unique id for serialization.
     */
    private static final long serialVersionUID = 84084596791289548L;

    private static final int NTLM_TOKEN_MAX_LENGTH = 8;

    private static final Byte CHAR_S_BYTE = (byte) 'S';

    /** The ntlmssp signature. */
    private static final Byte[] NTLMSSP_SIGNATURE = {(byte) 'N',
            (byte) 'T', (byte) 'L',
            (byte) 'M', CHAR_S_BYTE, CHAR_S_BYTE,
            (byte) 'P', (byte) 0};

    private static final Logger LOGGER = LoggerFactory.getLogger(SpnegoCredential.class);
    /**
     * The SPNEGO Init Token.
     */
    private byte[] initToken;

    /**
     * The SPNEGO Next Token.
     */
    private byte[] nextToken;

    /**
     * The Principal.
     */
    private Principal principal;

    /**
     * The authentication type should be Kerberos or NTLM.
     */
    private boolean isNtlm;

    /**
     * Instantiates a new SPNEGO credential.
     *
     * @param initToken the init token
     */
    public SpnegoCredential(final byte[] initToken) {
        Assert.notNull(initToken, "The initToken cannot be null.");
        this.initToken = consumeByteSourceOrNull(ByteSource.wrap(initToken));
        this.isNtlm = isTokenNtlm(this.initToken);
    }

    public byte[] getInitToken() {
        return this.initToken;
    }

    public byte[] getNextToken() {
        return this.nextToken;
    }

    /**
     * Sets next token.
     *
     * @param nextToken the next token
     */
    public void setNextToken(final byte[] nextToken) {
        this.nextToken = consumeByteSourceOrNull(ByteSource.wrap(nextToken));
    }

    public Principal getPrincipal() {
        return this.principal;
    }

    public void setPrincipal(final Principal principal) {
        this.principal = principal;
    }

    public boolean isNtlm() {
        return this.isNtlm;
    }

    @Override
    public String getId() {
        return this.principal != null ? this.principal.getId() : UNKNOWN_ID;
    }

    @Override
    public String toString() {
        return getId();
    }

    /**
     * Checks if is token ntlm.
     *
     * @param token the token
     * @return true, if  token ntlm
     */
    private static boolean isTokenNtlm(final byte[] token) {
        if (token == null || token.length < NTLM_TOKEN_MAX_LENGTH) {
            return false;
        }
        return IntStream.range(0, NTLM_TOKEN_MAX_LENGTH).noneMatch(i -> NTLMSSP_SIGNATURE[i] != token[i]);
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null || !obj.getClass().equals(this.getClass())) {
            return false;
        }

        final SpnegoCredential c = (SpnegoCredential) obj;

        return Arrays.equals(this.getInitToken(), c.getInitToken())
                && this.principal.equals(c.getPrincipal())
                && Arrays.equals(this.getNextToken(), c.getNextToken());
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        if (this.principal != null) {
            hash = this.principal.hashCode();
        }
        return new HashCodeBuilder().append(this.getInitToken())
            .append(this.getNextToken())
            .append(hash).toHashCode();
    }

    /**
     * Read the contents of the source into a byte array.
     * @param source  the byte array source
     * @return the byte[] read from the source or null
     */
    private static byte[] consumeByteSourceOrNull(final ByteSource source) {
        try {
            if (source == null || source.isEmpty()) {
                return null;
            }
            return source.read();
        } catch (final IOException e) {
            LOGGER.warn("Could not consume the byte array source", e);
            return null;
        }
    }
}
