/*
 * 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.vw.services.domain.model.authentication;

import org.apache.commons.lang3.exception.ExceptionUtils;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonValue;
import java.io.IOException;

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.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.DuplicateEntryException;
import com.scytl.products.ov.commons.beans.exceptions.EntryPersistenceException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.logging.service.AuthTokenHashService;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants;
import com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents;
import com.scytl.products.ov.vw.services.domain.model.ballot.BallotRepository;
import com.scytl.products.ov.vw.services.domain.model.ballot.BallotTextRepository;
import com.scytl.products.ov.vw.services.domain.model.ballotbox.BallotBoxInformationRepository;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardState;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStateRepository;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStates;
import com.scytl.products.ov.vw.services.domain.model.verification.Verification;
import com.scytl.products.ov.vw.services.domain.model.verification.VerificationRepository;
import com.scytl.products.ov.vw.services.domain.model.verificationset.VerificationSet;
import com.scytl.products.ov.vw.services.domain.model.verificationset.VerificationSetRepository;
import com.scytl.products.ov.vw.services.domain.service.VotingCardStateService;

/**
 * Service for generating the authentication token.
 */
@Stateless
public class AuthenticationTokenService {

    private static final String VERIFICATION_CARD = "verificationCard";

    private static final String VERIFICATION_CARD_SET = "verificationCardSet";

    private static final String BALLOT_BOX = "ballotBox";
    
    private static final String BALLOT_TEXTS = "ballotTexts";

    private static final String BALLOT_TEXTS_SIGNATURE = "ballotTextsSignature";

    private static final String BALLOT = "ballot";

    private static final String AUTHENTICATION_TOKEN = "authenticationToken";

    private static final String VOTING_CARD_STATE = "votingCardState";

    private static final String VALIDATION_ERROR = "validationError";

    // The EJB instance for retrieving authentication tokens
    @EJB
    private AuthenticationTokenRepository authenticationTokenRepository;

    // The EJB instance for retrieving ballot information
    @EJB
    private BallotRepository ballotRepository;

    // The EJB instance for retrieving ballot information
    @EJB
    private BallotBoxInformationRepository ballotBoxInformationRepository;

    // The EJB instance for retrieving ballot information
    @EJB
    private VerificationRepository verificationRepository;

    // The EJB instance for retrieving ballot text
    @EJB
    private BallotTextRepository ballotTextRepository;

    // The EJB instance for retrieving verification card sets
    @EJB
    private VerificationSetRepository verificationSetRepository;

    // The voting card state service
    @EJB
    private VotingCardStateService votingCardStateService;

    // The voting card state service
    @EJB
    private VotingCardStateRepository votingCardStateRepository;

    @Inject
    private TrackIdInstance trackId;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private AuthTokenHashService authTokenHashService;

    /**
     * Gets the authentication token which matches with the given parameters.
     *
     * @param tenantId             - the tenant identifier.
     * @param electionEventId      - the election event identifier.
     * @param credentialId         - the credential Identifier.
     * @param challengeInformation - the challenge information including client challenge, server
     *                             challenge, and server timestamp.
     * @return json object represeting the authentication token.
     * @throws ApplicationException
     * @throws ResourceNotFoundException
     * @throws IOException
     * @throws DuplicateEntryException
     * @throws GeneralCryptoLibException if fails generating hash of the auth token.
     * @throws EntryPersistenceException
     */
    public JsonObject getAuthenticationToken(String tenantId, String electionEventId, String credentialId,
                                             ChallengeInformation challengeInformation)
        throws ApplicationException, ResourceNotFoundException, IOException, DuplicateEntryException,
        GeneralCryptoLibException, EntryPersistenceException {

        // build the authentication token
        AuthenticationTokenMessage authTokenMessage =
            buildAuthenticationToken(tenantId, electionEventId, credentialId, challengeInformation);

        //assuming the authToken is always present even if it fails 'validation'
        // auth token
        AuthenticationToken authToken = authTokenMessage.getAuthenticationToken();
        JsonObject authTokenJsonObject = JsonUtils.getJsonObject(ObjectMappers.toJson(authToken));
        
        // get the voting card id from the voter information
        String votingCardId = authToken.getVoterInformation().getVotingCardId();

        // recover the voting card state
        VotingCardState votingCardState =
            votingCardStateService.getVotingCardState(tenantId, electionEventId, votingCardId);

        // initialize the voting card state
        votingCardStateService.initializeVotingCardState(votingCardState);
        VotingCardStates votingCardStateValue = votingCardState.getState();
        // always include the validation result
        JsonValue validationJson =
            JsonUtils.getJsonObject(ObjectMappers.toJson(authTokenMessage.getValidationError()));

        // create object response
        JsonObjectBuilder authTokenObjectBuilder = Json.createObjectBuilder();
        authTokenObjectBuilder.add(AUTHENTICATION_TOKEN, authTokenJsonObject)
            .add(VALIDATION_ERROR, validationJson).add(VOTING_CARD_STATE, votingCardStateValue.name());

        //#9866 - always add the ballot information to the result so that the FE can either show the ballot info or
        // request the receipt even if the election if over. it would be nice to configure this dynamically
        addBallotAndVerificationDataToAuthToken(tenantId, electionEventId, votingCardId, authToken, authTokenObjectBuilder);

        //sent ballot.
        String authTokenHash = authTokenHashService.hash(authTokenJsonObject);
        secureLoggerWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.SENT_BALLOT).objectId(authTokenHash)
                .user(credentialId).electionEvent(electionEventId)
                .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardStateValue.name())
                .createLogInfo());

        return  authTokenObjectBuilder.build();
    }

    public ValidationResult validateAuthenticationToken(final String tenantId, final String electionEventId,
                                                        final String votingCardId, final String authenticationToken)
        throws IOException, ResourceNotFoundException, ApplicationException {

        return authenticationTokenRepository.validateAuthenticationToken(tenantId, electionEventId, votingCardId,
            authenticationToken);

    }

    // Generate authentication token.
    private AuthenticationTokenMessage buildAuthenticationToken(String tenantId, String electionEventId,
                                                                String credentialId, ChallengeInformation challengeInformation)
        throws IOException, ResourceNotFoundException, ApplicationException {
        return authenticationTokenRepository.getAuthenticationToken(tenantId, electionEventId, credentialId,
                challengeInformation);
    }

    // Gets the verification card set data json.
    private JsonObject getVerificationCardSetDataJson(String tenantId, String electionEventId, String votingCardId,
                                                      String verificationCardSetId) throws IOException, ResourceNotFoundException {
        try {
            VerificationSet verificationCardSet = verificationSetRepository
                .findByTenantElectionEventVerificationCardSetId(tenantId, electionEventId, verificationCardSetId);
            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder()
                    .logEvent(VotingWorkflowLogEvents.VERIFICATION_CARD_SET_RESOURCE_FOUND).objectId(votingCardId)
                    .user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, verificationCardSetId)
                    .createLogInfo());
            return JsonUtils.getJsonObject(ObjectMappers.toJson(verificationCardSet));
        } catch (ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(VotingWorkflowLogEvents.VERIFICATION_CARD_SET_RESOURCE_NOT_FOUND).objectId(votingCardId)
                    .user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, verificationCardSetId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                        "Verification card set data not found: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw e;
        }
    }

    // Gets the verification card data json.
    private JsonObject getVerificationCardDataJson(String tenantId, String electionEventId, String votingCardId,
                                                   String verificationCardId) throws IOException, ResourceNotFoundException {
        try {
            Verification verificationCard = verificationRepository.findByTenantElectionEventVotingCard(tenantId,
                electionEventId, verificationCardId);
            secureLoggerWriter
                .log(Level.INFO,
                    new LogContent.LogContentBuilder()
                        .logEvent(VotingWorkflowLogEvents.VERIFICATION_CARD_RESOURCE_FOUND).objectId(votingCardId)
                        .user(votingCardId).electionEvent(electionEventId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, verificationCardId)
                        .createLogInfo());
            return JsonUtils.getJsonObject(ObjectMappers.toJson(verificationCard));
        } catch (ResourceNotFoundException e) {
            secureLoggerWriter
                .log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(VotingWorkflowLogEvents.VERIFICATION_CARD_RESOURCE_NOT_FOUND).objectId(votingCardId)
                        .user(votingCardId).electionEvent(electionEventId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, verificationCardId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                            "Verification card data not found: " + ExceptionUtils.getRootCauseMessage(e))
                        .createLogInfo());
            throw e;
        }
    }

    // Gets the ballot box json.
    private JsonObject getBallotBoxJson(String tenantId, String electionEventId, String votingCardId,
                                        String ballotBoxId) throws ResourceNotFoundException {
        try {
            String ballotBox = ballotBoxInformationRepository
                .getBallotBoxInfoByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId);
            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.BALLOT_BOX_RESOURCE_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, ballotBoxId)
                    .createLogInfo());
            return JsonUtils.getJsonObject(ballotBox);
        } catch (ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.BALLOT_BOX_RESOURCE_NOT_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, ballotBoxId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                        "Ballot box not found: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw e;
        }
    }

    // Gets the ballot json.
    private JsonObject getBallotJson(String tenantId, String electionEventId, String votingCardId, String ballotId)
        throws ResourceNotFoundException {
        try {
            String ballot = ballotRepository.findByTenantIdElectionEventIdBallotId(tenantId, electionEventId, ballotId);
            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.BALLOT_RESOURCE_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, ballotId)
                    .createLogInfo());
            return JsonUtils.getJsonObject(ballot);
        } catch (ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.BALLOT_RESOURCE_NOT_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, ballotId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                        "Ballot not found: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw e;
        }
    }

    // Gets the ballot text json.
    private String getBallotText(String tenantId, String electionEventId, String votingCardId, String ballotId)
        throws ResourceNotFoundException {
        try {
            final String ballotText = ballotTextRepository.findByTenantIdElectionEventIdBallotId(tenantId, electionEventId, ballotId);
            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.BALLOT_TEXT_RESOURCE_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, ballotId)
                    .createLogInfo());
            return ballotText;
        } catch (ResourceNotFoundException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.BALLOT_TEXT_RESOURCE_NOT_FOUND)
                    .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_RESOURCE_ID, ballotId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                        "Ballot text not found: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw e;
        }
    }

    /**
     * Add the ballot and verification information to the authentication token.
     * @param tenantId the tenant
     * @param electionEventId the election
     * @param votingCardId the voting card
     * @param authToken the original authentication token
     * @param authTokenObjectBuilder the resulting authentication object
     * @throws ResourceNotFoundException
     * @throws IOException
     */
    private void addBallotAndVerificationDataToAuthToken(final String tenantId, final String electionEventId,
                                                         final String votingCardId, final AuthenticationToken authToken,
                                                         final JsonObjectBuilder authTokenObjectBuilder)
        throws ResourceNotFoundException, IOException {

        // search ballot representation extracting the info from the token
        String ballotId = authToken.getVoterInformation().getBallotId();
        JsonValue ballotJson = getBallotJson(tenantId, electionEventId, votingCardId, ballotId);

        // search ballot texts extracting the info from the token
        String ballotAndSignatureJson = getBallotText(tenantId, electionEventId, votingCardId, ballotId);
        JsonObject ballotAndSignatureJsonObject = JsonUtils.getJsonObject(ballotAndSignatureJson);
        JsonValue ballotTextJson = ballotAndSignatureJsonObject.getJsonArray(BALLOT_TEXTS);
        JsonValue ballotTextSignatureJson = ballotAndSignatureJsonObject.getJsonArray(BALLOT_TEXTS_SIGNATURE);

        // search ballot box representation extracting the info from the
        // token
        String ballotBoxId = authToken.getVoterInformation().getBallotBoxId();
        JsonValue ballotBoxJson = getBallotBoxJson(tenantId, electionEventId, votingCardId, ballotBoxId);

        // Obtains the verification card Data
        String verificationCardId = authToken.getVoterInformation().getVerificationCardId();
        JsonValue verificationCardJson =
            getVerificationCardDataJson(tenantId, electionEventId, votingCardId, verificationCardId);

        // Obtains the verification card set
        String verificationCardSetId = authToken.getVoterInformation().getVerificationCardSetId();
        JsonValue verificationCardSetJson =
            getVerificationCardSetDataJson(tenantId, electionEventId, votingCardId, verificationCardSetId);

        authTokenObjectBuilder.add(BALLOT, ballotJson).add(BALLOT_TEXTS, ballotTextJson)
            .add(BALLOT_TEXTS_SIGNATURE, ballotTextSignatureJson).add(BALLOT_BOX, ballotBoxJson)
            .add(VERIFICATION_CARD, verificationCardJson).add(VERIFICATION_CARD_SET, verificationCardSetJson);
    }

}
