/*
 * 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.ei.services.domain.service.vote;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.json.JsonObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.ValidationException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.dto.ComputeResults;
import com.scytl.products.ov.commons.dto.VoteAndComputeResults;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.commons.util.ValidationUtils;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.AdditionalData;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBox;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxInformation;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxInformationRepository;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.ei.services.domain.model.castcode.VoteCastCodeRepository;
import com.scytl.products.ov.ei.services.domain.model.vote.ReceiptFactory;
import com.scytl.products.ov.ei.services.domain.service.ballotbox.CleansedBallotBoxService;
import com.scytl.products.ov.ei.services.infrastructure.persistence.BallotBoxAccess;
import com.scytl.products.ov.ei.services.infrastructure.persistence.BallotBoxFactory;

/**
 * Implementation of the interface VoteService
 */
@Stateless(name = "ei-VoteService")
public class VoteServiceImpl implements VoteService {

    private static final String CONFIRMATION_REQUIRED = "confirmationRequired";

    private static final String RECEIPT = "receipt";

    // An instance of the receipt generator service
    @Inject
    private ReceiptFactory receiptFactory;

    @Inject
    private BallotBoxRepository ballotBoxRepository;

    @Inject
    private BallotBoxAccess ballotBoxAccess;

    @Inject
    private CleansedBallotBoxService cleansedBallotBoxService;

    @Inject
    private BallotBoxInformationRepository ballotBoxInformationRepository;

    @Inject
    private VoteCastCodeRepository voteCastCodeRepository;

    @Inject
    private BallotBoxFactory ballotBoxFactory;

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

    /**
     * @see VoteService#getVoteReceipt(String, String, String)
     */
    @Override
    public Receipt getVoteReceipt(String tenantId, String electionEventId, String votingCardId)
            throws ResourceNotFoundException, ApplicationException {

        // Check if there's a vote
        BallotBox ballotBoxItem;
        try {
            ballotBoxItem =
                ballotBoxRepository.findByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId, votingCardId);
        } catch (ResourceNotFoundException e) {
            throw new ApplicationException("Invalid operation exception: vote not sent yet", e);
        }

        String ballotBoxId = ballotBoxItem.getBallotBoxId();

        // Check if the operation is valid
        boolean confirmationRequired = isConfirmationRequired(tenantId, electionEventId, ballotBoxId);

        if (confirmationRequired) {
            try {
                voteCastCodeRepository.findByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId,
                    votingCardId);
            } catch (ResourceNotFoundException e) {
                throw new ApplicationException("Invalid operation: confirmation not done yet", e);
            }
        }

        // Retrieve the receipt from the json-formated vote
        String jsonString = ballotBoxItem.getVote();
        return getReceiptFromJson(jsonString);
    }

    private boolean isConfirmationRequired(String tenantId, String electionEventId, String ballotBoxId)
            throws ResourceNotFoundException {
        BallotBoxInformation ballotBoxInformation = ballotBoxInformationRepository
            .findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId);

        JsonObject jsonObject = JsonUtils.getJsonObject(ballotBoxInformation.getJson());

        return jsonObject.getBoolean(CONFIRMATION_REQUIRED);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public Receipt saveVoteGenerateReceipt(VoteAndComputeResults voteAndComputeResults, String authenticationToken)
            throws DuplicateEntryException, ValidationException, ApplicationException, ResourceNotFoundException {

        Vote vote = voteAndComputeResults.getVote();
        ComputeResults computationResults = voteAndComputeResults.getComputeResults();

        // validate vote
        ValidationUtils.validate(vote);

        boolean confirmationRequired;

        confirmationRequired =
            isConfirmationRequired(vote.getTenantId(), vote.getElectionEventId(), vote.getBallotBoxId());

        // remove any sensitive information before storing the vote (modifies
        // original entity)
        cleanVoteIfNecesary(vote, confirmationRequired);

        LOG.info("Storing the vote for tenant: {} and ballot: {} into ballotBox: {}.", vote.getTenantId(),
            vote.getBallotId(), vote.getBallotBoxId());

        // generate the receipt
        Receipt receipt;

        try {
            receipt = receiptFactory.generate(vote, authenticationToken);

            LOG.info("Generated receipt {} for the vote with tenant: {} and ballot: {} into ballotBox: {}.",
                receipt.getReceipt(), vote.getTenantId(), vote.getBallotId(), vote.getBallotBoxId());

            // sign the receipt
            receipt.setSignature(receiptFactory.sign(receipt, vote));

            LOG.info("Receipt sucessfully signed for the vote with tenant: {} and ballot: {} into ballotBox: {}.",
                vote.getTenantId(), vote.getBallotId(), vote.getBallotBoxId());

        } catch (CryptographicOperationException e) {
            throw new ApplicationException("There was a problem signing the receipt", e);
        }

        List<AdditionalData> listAdditionalData = new ArrayList<>();

        // store the vote
        BallotBox savedEntity = ballotBoxAccess
            .save(ballotBoxFactory.from(vote, computationResults, receipt, authenticationToken, listAdditionalData));

        if (!confirmationRequired) {
            LOG.info("Vote confirmation not required. Storing cleansed vote");
            cleansedBallotBoxService.storeCleansedVote(vote);
            cleansedBallotBoxService.storeSuccessfulVote(vote.getTenantId(), vote.getElectionEventId(),
                vote.getBallotBoxId(), vote.getVotingCardId(), receipt.getReceipt());
        }

        LOG.info("Stored vote with receipt {} for tenant: {} and ballot: {} into ballotBox: {}.", receipt.getReceipt(),
            vote.getTenantId(), vote.getBallotId(), vote.getBallotBoxId());

        String jsonString = savedEntity.getVote();
        return getReceiptFromJson(jsonString);
    }

    @Override
    public VoteAndComputeResults retrieveVote(String tenantId, String electionEventId, String votingCardId)
            throws ApplicationException, ResourceNotFoundException {

        LOG.info(
            "Attempting to retrieve partial decryption results from ballot box for tenant: {} and election event: {} and voting card: {}",
            tenantId, electionEventId, votingCardId);

        BallotBox ballotBox;
        try {
            ballotBox =
                ballotBoxRepository.findByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId, votingCardId);
        } catch (ResourceNotFoundException e) {
            LOG.info("Did not find the vote with partial decryption results in ballot box");
            throw e;
        }

        try {

            VoteAndComputeResults voteAndComputeResults = ballotBoxFactory.to(ballotBox);

            LOG.info("Successfully obtained the vote with partial decryption results");

            return voteAndComputeResults;

        } catch (IOException e) {

            String errorMsg = "Exception when trying to recover the vote with partial decryption results";
            LOG.error(errorMsg);
            throw new ApplicationException(errorMsg, e);
        }
    }

    private Receipt getReceiptFromJson(String jsonString) throws ApplicationException {

        JsonObject voteJson = JsonUtils.getJsonObject(jsonString);
        JsonObject receiptJson = voteJson.getJsonObject(RECEIPT);

        if (receiptJson == null || receiptJson.isEmpty()) {
            throw new ApplicationException("Inconsistency on server db : vote without receipt");
        }

        try {
            return ObjectMappers.fromJson(receiptJson.toString(), Receipt.class);
        } catch (IOException e) {
            throw new ApplicationException("Error parsing the receipt", e);
        }
    }

    private void cleanVoteIfNecesary(Vote vote, boolean confirmationRequired)
            throws ApplicationException, ResourceNotFoundException {
        // We can only clean the vote when no retries to get the choice codes
        // are necessary.
        // As having confirmation phase without retries would be a enormous
        // usability problem, we can only clean the vote when no confirmation is
        // required.

        if (!confirmationRequired) {
            vote.setCipherTextExponentiations(null);
            vote.setEncryptedPartialChoiceCodes(null);
            vote.setExponentiationProof(null);
            vote.setPlaintextEqualityProof(null);
        }

    }

}
