/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.au.services.domain.model.authentication;

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.json.JsonObjectBuilder;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.exception.ExceptionUtils;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.au.services.domain.model.adminboard.AdminBoard;
import com.scytl.products.ov.au.services.domain.model.adminboard.AdminBoardRepository;
import com.scytl.products.ov.au.services.domain.model.challenge.ServerChallengeMessage;
import com.scytl.products.ov.au.services.domain.model.material.Credential;
import com.scytl.products.ov.au.services.domain.model.material.CredentialRepository;
import com.scytl.products.ov.au.services.infrastructure.log.AuthenticationLogConstants;
import com.scytl.products.ov.au.services.infrastructure.log.AuthenticationLogEvents;
import com.scytl.products.ov.au.services.infrastructure.remote.AuRemoteCertificateService;
import com.scytl.products.ov.commons.beans.domain.model.certificate.CertificateEntity;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.crypto.SignatureForObjectService;
import com.scytl.products.ov.commons.infrastructure.remote.service.RemoteCertificateService;
import com.scytl.products.ov.commons.random.RandomType;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.util.DateUtils;
import com.scytl.products.ov.commons.util.JsonUtils;

/**
 * Factory for building authentication information.
 */
@Stateless
public class CredentialInformationFactory {

    private static final String KEYSTORE_ALIAS = "privatekey";

    @Inject
    private CredentialRepository credentialRepository;

    @Inject
    private SignatureForObjectService signatureService;

    @Inject
    private AuthenticationCertsRepository authenticationCertsRepository;

    @Inject
    private TrackIdInstance trackId;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private AdminBoardRepository adminBoardRepository;

    @Inject
    @AuRemoteCertificateService
    private RemoteCertificateService remoteCertificateService;

    private static final String EMPTY_ID = "";

    public static final String ADMINISTRATION_BOARD_CN_PREFIX = "AdministrationBoard ";

    /**
     * Builds an credential information according with the set of input parameters.
     *
     * @param tenantId
     *            - the identifier of the tenant.
     * @param electionEventId
     *            - the identifier of the election event.
     * @param credentialId
     *            - the identifier of the credential.
     * @param randomType
     *            - the random type for the challenge.
     * @param randomValueLength
     *            - the length of the generated random value.
     * @return an credential information setting up with the corresponding information.
     * @throws GeneralCryptoLibException
     *             if length is out of the range for this generator.
     * @throws ResourceNotFoundException
     *             - if credential data which is part of authentication information can not be found.
     * @throws CryptographicOperationException
     *             - if an error occurred during the signature of the challenge
     */
    public CredentialInformation buildCredentialInformation(final String tenantId, final String electionEventId,
            final String credentialId, final RandomType randomType, final int randomValueLength)
            throws GeneralCryptoLibException, ResourceNotFoundException, CryptographicOperationException {

        // get credential data
        Credential credentialData;
        try {
            credentialData =
                credentialRepository.findByTenantIdElectionEventIdCredentialId(tenantId, electionEventId, credentialId);
        } catch (ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.ERROR_SERVER_CHALLENGE_GENERATION)
                    .objectId(credentialId).user(credentialId).electionEvent(electionEventId)
                    .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(AuthenticationLogConstants.INFO_CREDENTIAL_ID, credentialId)
                    .additionalInfo(AuthenticationLogConstants.INFO_ERR_DESC,
                        "Credential data not found from client: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw e;
        }

        secureLoggerWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.CREDENTIAL_DATA_FOUND)
                .objectId(credentialId).user(credentialId).electionEvent(electionEventId)
                .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(AuthenticationLogConstants.INFO_CREDENTIAL_ID, credentialId)
                .createLogInfo());

        // build server challenge message
        ServerChallengeMessage serverChallengeMessage;
        try {
            Challenge challenge = new Challenge(randomType, randomValueLength);
            String currentTimestamp = DateUtils.getTimestamp();

            byte[] signedChallenge = signatureService.sign(tenantId, electionEventId, EMPTY_ID, KEYSTORE_ALIAS,
                challenge.getChallengeValue(), currentTimestamp, electionEventId, credentialId);
            String base64SignedChallenge = Base64.encodeBase64String(signedChallenge);
            serverChallengeMessage =
                new ServerChallengeMessage(challenge.getChallengeValue(), currentTimestamp, base64SignedChallenge);
        } catch (GeneralCryptoLibException e) {
            secureLoggerWriter
                .log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(AuthenticationLogEvents.ERROR_SERVER_CHALLENGE_GENERATION).objectId(credentialId)
                        .user(credentialId).electionEvent(electionEventId)
                        .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                        .additionalInfo(AuthenticationLogConstants.INFO_CREDENTIAL_ID, credentialId)
                        .additionalInfo(AuthenticationLogConstants.INFO_ERR_DESC,
                            "Error generating the challenge: " + ExceptionUtils.getRootCauseMessage(e))
                        .createLogInfo());

            throw e;
        } catch (CryptographicOperationException | ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.ERROR_SERVER_CHALLENGE_GENERATION)
                    .objectId(credentialId).user(credentialId).electionEvent(electionEventId)
                    .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(AuthenticationLogConstants.INFO_CREDENTIAL_ID, credentialId)
                    .additionalInfo(AuthenticationLogConstants.INFO_ERR_DESC,
                        "Error signing the server challenge: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw e;
        }

        AuthenticationCerts certificates = buildCertificatesInfo(tenantId, electionEventId, credentialId);

        secureLoggerWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.CERTIFICATES_FOUND)
                .objectId(credentialId).user(credentialId).electionEvent(electionEventId)
                .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(AuthenticationLogConstants.INFO_CREDENTIAL_ID, credentialId)
                .createLogInfo());

        // build credential information
        CredentialInformation credentialInformation = new CredentialInformation(credentialData, serverChallengeMessage,
            certificates.getJson(), certificates.getSignature());

        secureLoggerWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.SUCCESSFUL_SERVER_CHALLENGE_GENERATION)
                .objectId(credentialId).user(credentialId).electionEvent(electionEventId)
                .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(AuthenticationLogConstants.INFO_CREDENTIAL_ID, credentialId).createLogInfo());

        return credentialInformation;
    }

    private AuthenticationCerts buildCertificatesInfo(final String tenantId, final String electionEventId,
            final String votingCardId) throws ResourceNotFoundException {
        // get certificates
        AuthenticationCerts certificates;
        try {
            certificates = authenticationCertsRepository.findByTenantIdElectionEventId(tenantId, electionEventId);
            AdminBoard adminBoard = adminBoardRepository.findByTenantIdElectionEventId(tenantId, electionEventId);
            final String adminBoardCommonName = ADMINISTRATION_BOARD_CN_PREFIX + adminBoard.getAdminBoardId();
            CertificateEntity adminBoardCertificateEntity =
                remoteCertificateService.getAdminBoardCertificate(adminBoardCommonName);
            CertificateEntity tenantCA = remoteCertificateService.getTenantCACertificate(tenantId);

            JsonObjectBuilder certificateJsonBuilder =
                JsonUtils.jsonObjectToBuilder(JsonUtils.getJsonObject(certificates.getJson()));
            certificateJsonBuilder.add("adminBoard", adminBoardCertificateEntity.getCertificateContent());
            certificateJsonBuilder.add("tenantCA", tenantCA.getCertificateContent());
            certificates.setJson(certificateJsonBuilder.build().toString());

        } catch (ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.CERTIFICATES_NOT_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(AuthenticationLogConstants.INFO_ERR_DESC,
                        "Resource not found in the AUTHENTICATION_CERTS table.")
                    .createLogInfo());
            throw e;
        }
        return certificates;
    }

    /**
     * Builds an credential information according with the set of input parameters.
     *
     * @param tenantId
     *            - the identifier of the tenant.
     * @param electionEventId
     *            - the identifier of the election event.
     *            - the identifier of the credential.
     * @return an credential information setting up with the corresponding information.
     * @throws GeneralCryptoLibException
     *             if length is out of the range for this generator.
     * @throws ResourceNotFoundException
     *             - if credential data which is part of authentication information can not be found.
     * @throws CryptographicOperationException
     *             - if an error occurred during the signature of the challenge
     */
    public AuthenticationCerts buildAuthenticationCertificates(final String tenantId, final String electionEventId                                                )
            throws GeneralCryptoLibException, ResourceNotFoundException, CryptographicOperationException{

        return buildCertificatesInfo(tenantId, electionEventId, "");
    }
}
