package be.iminds.ilabt.jfed.lowlevel;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.userloginmodel.InvalidLoginException;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;

/**
 * SimpleGeniUserWithSpeaksFor
 */
public class SimpleGeniUserWithSpeaksFor extends SimpleGeniUser implements GeniUserWithSpeaksFor {
    private final static Logger LOG = LoggerFactory.getLogger(SimpleGeniUserWithSpeaksFor.class);

    protected final List<AnyCredential> speaksForCredentials;
    private GeniUrn speaksForUrn;
    private List<X509Certificate> speaksForCertificateChain;
    private PublicKey speaksForPublicKey;

    /**
     * Exception thrown when the provided speaks_for credential(s) are not valid for a reason
     */
    public static class InvalidSpeaksForException extends Exception {
        public InvalidSpeaksForException(String message) {
            super(message);
        }

        public InvalidSpeaksForException(String message, Throwable cause) {
            super(message, cause);
        }

        public InvalidSpeaksForException(Throwable cause) {
            super(cause);
        }
    }

    public SimpleGeniUserWithSpeaksFor(@Nullable Server userAuthorityServer, @Nonnull GeniUrn userUrn,
                                       @Nonnull String keyCertContent, @Nullable char[] keyPass,
                                       @Nullable File certificateKeyFile, @Nullable File privateKeyFile,
                                       @Nonnull List<AnyCredential> speaksForCredentials)
            throws InvalidSpeaksForException, InvalidLoginException {
        super(userAuthorityServer, userUrn, keyCertContent, keyPass, certificateKeyFile, privateKeyFile);
        this.speaksForCredentials = speaksForCredentials;

        assert !speaksForCredentials.isEmpty();

        deriveSpeaksForDetailsFromCredentials();
        assert speaksForUrn != null;
        assert speaksForCertificateChain != null;
        assert speaksForPublicKey != null;
    }

    public SimpleGeniUserWithSpeaksFor(@Nullable Server userAuthorityServer, @Nonnull GeniUrn userUrn,
                                       @Nonnull List<X509Certificate> certificateChain,
                                       @Nonnull PrivateKey privateKey,
                                       @Nullable File certificateKeyFile, @Nullable File privateKeyFile,
                                       @Nonnull List<AnyCredential> speaksForCredentials) throws InvalidSpeaksForException {
        super(userAuthorityServer, userUrn, certificateChain, privateKey, certificateKeyFile, privateKeyFile);
        this.speaksForCredentials = speaksForCredentials;

        assert !speaksForCredentials.isEmpty();

        deriveSpeaksForDetailsFromCredentials();
        assert speaksForUrn != null;
        assert speaksForCertificateChain != null;
        assert speaksForPublicKey != null;
    }

    public SimpleGeniUserWithSpeaksFor(@Nonnull GeniUserWithSpeaksFor geniUser) {
        super(geniUser);
        this.speaksForCredentials = geniUser.getSpeaksForCredentials();
        this.speaksForCertificateChain = geniUser.getSpeaksForUserCertificateChain();
        this.speaksForPublicKey = geniUser.getSpeaksForUserPublicKey();
        this.speaksForUrn = geniUser.getSpeaksForUrn();
    }


    @Nonnull
    @Override
    public List<AnyCredential> getSpeaksForCredentials() {
        return speaksForCredentials;
    }

    @Nonnull
    @Override
    public GeniUrn getSpeaksForUrn() {
        return speaksForUrn;
    }

    @Nonnull
    @Override
    public List<X509Certificate> getSpeaksForUserCertificateChain() {
        return speaksForCertificateChain;
    }

    @Nonnull
    @Override
    public PublicKey getSpeaksForUserPublicKey() {
        return speaksForPublicKey;
    }

    /**
     * Uses the provided speaksForCredentials to derive all speaksFor details
     * <p>
     * Will throw InvalidSpeaksForException when it encounters any credential or speaksFor error.
     * It will ignore some unknown credentials, and once it finds a valid speaks_for credential,
     * it will not process any other of the provided credentials.
     */
    private void deriveSpeaksForDetailsFromCredentials() throws InvalidSpeaksForException {
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            return;
        }

        for (AnyCredential anyS4Credential : speaksForCredentials) {
            if (anyS4Credential == null)
                throw new InvalidSpeaksForException("Experiment.speaksForCredential is not a valid credential. (null after parsing)");

            if (!(anyS4Credential instanceof AbacCredential)) {
                if (anyS4Credential instanceof SfaCredential && !anyS4Credential.isSpeaksFor()) {
                    throw new InvalidSpeaksForException("Experiment.speaksForCredential is a valid " +
                            anyS4Credential.getClass().getName() +
                            " credential, but only ABAC speaksFor credentials are currently supported. " +
                            "Additionally, the SfaCredential does not appear to be a speaksFor credential.");
                }
                //ignore non ABAC speaksFor
                //TODO: support SfaCredential speaksFor
                continue;
            }

            AbacCredential s4abac = (AbacCredential) anyS4Credential;

            if (!s4abac.isSpeaksFor()) {
                throw new InvalidSpeaksForException("Experiment.speaksForCredential is a valid ABAC credential, " +
                        "but it is not a speaksFor credential (which is required).");
            }

            Date now = new Date();
            if (s4abac.getExpiresDate() != null && s4abac.getExpiresDate().before(now)) {
                throw new InvalidSpeaksForException("Experiment.speaksForCredential expired at " + s4abac.getExpires());
            }

            try {
                this.speaksForCertificateChain = s4abac.getSignerCertificates();
                if (this.speaksForCertificateChain == null) {
                    throw new InvalidSpeaksForException("Experiment.speaksForCredential is not a valid ABAC speaksFor credential: " +
                            "the signer certificate(s) could not be found");
                }
            } catch (CredentialException e) {
                throw new InvalidSpeaksForException("Invalid speaks_for credential", e);
            }

            //check if all certificates are valid at this time
            for (X509Certificate certificate : speaksForCertificateChain) {
                try {
                    certificate.checkValidity();
                } catch (CertificateExpiredException e) {
                    LOG.error("The signer certificate in the speaks_for credential has expired." +
                            "(NotAfter= " + certificate.getNotAfter() + ")", e);
                    throw new InvalidSpeaksForException("The signer certificate in the speaks_for credential has expired. " +
                            "(NotAfter= " + certificate.getNotAfter() + ")", e);
                } catch (CertificateNotYetValidException e) {
                    throw new InvalidSpeaksForException("The signer certificate in the speaks_for credential is not yet valid " +
                            "(NotBefore=" + certificate.getNotBefore() + ")", e);
                }
            }

            this.speaksForUrn = s4abac.getSpokenForUrn();
            this.speaksForPublicKey = s4abac.getSpokenForPubKey();
            if (this.speaksForUrn == null) {
                throw new InvalidSpeaksForException("Experiment.speaksForCredential is a valid ABAC speaksFor credential, " +
                        "however the speaks_for user could not be derived from it.");
            }
            if (this.speaksForPublicKey == null) {
                throw new InvalidSpeaksForException("Experiment.speaksForCredential is a valid ABAC speaksFor credential, " +
                        "however the speaks_for public key could not be derived from it.");
            }

            if (speaksForCertificateChain != null) {
                //all speaksFor info found
                break;
            }
        }

        if (speaksForCertificateChain == null || speaksForPublicKey == null || speaksForUrn == null) {
            throw new InvalidSpeaksForException("No speaksFor details could be derived from the provided speaksFor credentials.");
        }
    }
}
