/*
 * 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.service;

import static com.scytl.products.oscore.logging.api.domain.Level.ERROR;
import static com.scytl.products.oscore.logging.api.domain.Level.INFO;

import java.io.IOException;

import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.domain.model.receipt.Receipt;
import com.scytl.products.ov.commons.beans.domain.model.vote.Vote;
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.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.exceptions.VoteRepositoryException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.dto.ChoiceCodeAndComputeResults;
import com.scytl.products.ov.commons.dto.ComputeResults;
import com.scytl.products.ov.commons.dto.VoteAndComputeResults;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.vw.infrastructure.log.SecureLoggerInformation;
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.choicecode.ChoiceCodeRepository;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardState;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStateEvaluator;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStates;
import com.scytl.products.ov.vw.services.domain.model.validation.ValidationRepository;
import com.scytl.products.ov.vw.services.domain.model.vote.ValidationVoteResult;
import com.scytl.products.ov.vw.services.domain.model.vote.VoteRepository;

@Stateless(name = "vw-voteService")
public class VoteServiceImpl implements VoteService {

    @Inject
    private VotingCardStateService votingCardStateService;

    @Inject
    private VoteRepository voteRepository;

    @Inject
    private ChoiceCodeRepository choiceCodeRepository;

    @Inject
    private ValidationRepository validationRepository;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private SecureLoggerInformation secureLoggerInformation;

    @Inject
    private TrackIdInstance trackId;

    private static final Logger LOG = LoggerFactory.getLogger("std");

    /**
     * Determines if we should ask for the receipt according to the current
     * voting card state
     *
     * @param tenantId
     * @param electionEventId
     * @param votingCardId
     * @return
     * @throws ResourceNotFoundException
     * @throws ApplicationException
     */
    private boolean canReceiptBeObtained(String tenantId, String electionEventId, String votingCardId)
            throws ResourceNotFoundException, ApplicationException {

        VotingCardState votingCardState =
            votingCardStateService.getVotingCardState(tenantId, electionEventId, votingCardId);

        VotingCardStates state = votingCardState.getState();
        return VotingCardStates.SENT_BUT_NOT_CAST.equals(state) || VotingCardStates.CAST.equals(state);

    }

    /**
     * Recovers the receipt of a vote
     *
     * @param tenantId
     * @param electionEventId
     * @param votingCardId
     * @return
     * @throws VoteRepositoryException
     */
    @Override
    public Receipt obtainReceipt(String tenantId, String electionEventId, String votingCardId)
            throws ResourceNotFoundException, ApplicationException, VoteRepositoryException {
        if (canReceiptBeObtained(tenantId, electionEventId, votingCardId)) {
            return voteRepository.getReceiptByVotingCardId(tenantId, electionEventId, votingCardId);
        } else {
            throw new ApplicationException("The receipt can't be obtained for this voting card state");
        }
    }

    /**
     * Validates and saves the vote
     *
     * @param tenantId
     * @param electionEventId
     * @param votingCardId
     * @param verificationCardId
     * @param vote
     * @param authenticationTokenJsonString
     * @return
     * @throws ApplicationException
     * @throws ResourceNotFoundException
     * @throws DuplicateEntryException
     * @throws IOException
     */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public ValidationVoteResult validateVoteAndStore(String tenantId, String electionEventId, String votingCardId,
            String verificationCardId, Vote vote, String authenticationTokenJsonString)
            throws ApplicationException, ResourceNotFoundException, DuplicateEntryException, IOException {

        VotingCardState votingCardState =
            votingCardStateService.getVotingCardState(tenantId, electionEventId, votingCardId);

        ValidationVoteResult validationVoteResult = new ValidationVoteResult();

        VotingCardStateEvaluator votingCardStateEvaluator =
            VotingCardStateEvaluator.forState(votingCardState.getState());

        // if the voting card state is blocked, 'return' immediately. if
        // NOT,also check the Vote 'state' and if both
        // states do not agree, the Vote state is authoritative

        if (votingCardStateEvaluator.votingCardIsBlocked()) {

            secureLoggerWriter.log(INFO, new LogContent.LogContentBuilder()
                .logEvent(VotingWorkflowLogEvents.VOTE_VALIDATION_INVALID_VOTING_CARD_STATE)
                .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                .electionEvent(electionEventId)
                .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC, "Wrong voting card state")
                .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                .createLogInfo());

            final ValidationError validationError = new ValidationError(ValidationErrorType.INVALID_VOTING_CARD_ID);
            validationError.setErrorArgs(new String[] {VotingCardStates.BLOCKED.name() });
            validationVoteResult.setValid(false);
            validationVoteResult.setValidationError(validationError);

            return validationVoteResult;
        }

        if (votingCardStateEvaluator.votingCardDidNotVote()) {

            validationVoteResult = validationRepository.validateVote(tenantId, electionEventId, vote);

            if (validationVoteResult.isValid()) {

                try {
                    final VoteAndComputeResults voteAndComputeResults = new VoteAndComputeResults();
                    voteAndComputeResults.setVote(vote);

                    // generate choice codes
                    ChoiceCodeAndComputeResults generatedChoiceCodes = choiceCodeRepository
                        .generateChoiceCodes(tenantId, electionEventId, verificationCardId, voteAndComputeResults);

                    validationVoteResult.setChoiceCodes(generatedChoiceCodes.getChoiceCodes());

                    secureLoggerWriter.log(INFO,
                        new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.CHOICE_CODES_RETRIEVED)
                            .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                            .electionEvent(electionEventId)
                            .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                            .additionalInfo(VotingWorkflowLogConstants.INFO_VERIFICATION_CARD_ID, verificationCardId)
                            .createLogInfo());

                    voteAndComputeResults.setComputeResults(new ComputeResults());
                    voteAndComputeResults.getComputeResults()
                        .setComputationResults(generatedChoiceCodes.getComputationResults());
                    voteAndComputeResults.getComputeResults()
                        .setDecryptionResults(generatedChoiceCodes.getDecryptionResults());

                    // only store the vote in the ballot box after the choice
                    // codes have been successfully generated
                    // store the vote and generate receipt
                    // US 2633 -> only in case the save does not return
                    // error, it should follow with the choice
                    // code generation
                    voteRepository.save(tenantId, electionEventId, voteAndComputeResults,
                        authenticationTokenJsonString);
                    // change voting card state to SENT BUT NOT CAST
                    votingCardStateService.updateVotingCardState(tenantId, electionEventId, votingCardId,
                        VotingCardStates.SENT_BUT_NOT_CAST);

                } catch (EJBException | ResourceNotFoundException e) {

                    validationVoteResult.setValid(Boolean.FALSE);
                    validationVoteResult.setValidationError(new ValidationError(ValidationErrorType.FAILED));
                    secureLoggerWriter.log(ERROR,
                        new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.STOP_VOTING_PROCESS)
                            .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                            .electionEvent(electionEventId)
                            .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                            .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                                "Error calling to choice code generation service: "
                                    + ExceptionUtils.getRootCauseMessage(e))
                            .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD,
                                votingCardState.getState().name())
                            .createLogInfo());

                }
            } else {
                secureLoggerWriter.log(INFO,
                    new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.STOP_VOTING_PROCESS)
                        .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                        .electionEvent(electionEventId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC, "Error validating the vote")
                        .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD,
                            votingCardState.getState().name())
                        .createLogInfo());
            }

        } else {

            LOG.info("VotingCard [tenant={}, election={}, votingCardId={}] already voted.", tenantId, electionEventId,
                votingCardId);
            secureLoggerWriter.log(INFO, new LogContent.LogContentBuilder()
                .logEvent(VotingWorkflowLogEvents.VOTE_VALIDATION_INVALID_VOTING_CARD_STATE)
                .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                .electionEvent(electionEventId)
                .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC, "Wrong voting card state")
                .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                .createLogInfo());
        }

        return validationVoteResult;

    }
}
