/*
 * 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.ui.ws.rs.operation;

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.EJB;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.json.JsonObject;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

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

import com.google.gson.Gson;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationExceptionMessages;
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.SemanticErrorException;
import com.scytl.products.ov.commons.beans.exceptions.SyntaxErrorException;
import com.scytl.products.ov.commons.beans.exceptions.ValidationException;
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.VoteAndComputeResults;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.Constants;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.HttpRequestService;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.commons.util.ValidationUtils;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.commons.validation.ValidationResult;
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.authentication.AuthenticationToken;
import com.scytl.products.ov.vw.services.domain.model.authentication.AuthenticationTokenService;
import com.scytl.products.ov.vw.services.domain.model.ballotbox.BallotBoxInformationRepository;
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.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;
import com.scytl.products.ov.vw.services.domain.service.VotingCardStateService;

/**
 * The end point for getting Choice Codes.
 */
@Path("/choicecodes")
@Stateless(name = "vw-ChoiceCodeResource")
public class ChoiceCodeResource {

    // JSON attribute for vote
    private static final String VOTE = "vote";

    // The name of the parameter value tenant id.
    private static final String PARAMETER_VALUE_TENANT_ID = "tenantId";

    // The name of the parameter value election event id.
    private static final String PARAMETER_VALUE_ELECTION_EVENT_ID = "electionEventId";

    // The name of the parameter value voting card id.
    private static final String PARAMETER_VALUE_VOTING_CARD_ID = "votingCardId";

    // The name of the parameter value authentication token.
    private static final String PARAMETER_AUTHENTICATION_TOKEN = "authenticationToken";

    // The date separator
    private static final String DATE_SEPARATOR = "|";

    // The name of the json parameter dateFrom.
    private static final String JSON_PARAMETER_DATE_FROM = "startDate";

    // The name of the json parameter dateTo.
    private static final String JSON_PARAMETER_DATE_TO = "endDate";

    @Inject
    private Logger LOG;

    // The choice code repository
    @EJB
    private ChoiceCodeRepository choiceCodeRepository;

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

    // The ballot box information repository
    @EJB
    private BallotBoxInformationRepository ballotBoxInformationRepository;

    // The vote repository
    @EJB
    private VoteRepository voteRepository;

    // The validation repository
    @EJB
    private ValidationRepository validationRepository;

    // The authentication token repository
    @Inject
    private AuthenticationTokenService authenticationTokenService;

    // Tracking id used for logging and tracking purposes
    @Inject
    private TrackIdInstance trackIdInstance;

    @Inject
    private TransactionInfoProvider transactionInfoProvider;

    @Inject
    private HttpRequestService httpRequestService;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private SecureLoggerInformation secureLoggerInformation;

    /**
     * Gets the generated choice codes which matches with the given parameters
     * and voting card state in SENT_BUT_NOT_CAST.
     *
     * @param tenantId
     *            - the tenant identifier.
     * @param electionEventId
     *            - the election event identifier.
     * @param votingCardId
     *            - the voting card Identifier.
     * @param authenticationTokenJsonString
     *            the authentication token json in string format.
     * @param request
     *            - the http servlet request.
     * @return if the operation is successfully performed, the generated choice
     *         codes
     * @throws GeneralCryptoLibException
     *             if length is out of the range for this generator during
     *             creation of token or in requestId generation.
     * @throws ApplicationException
     *             if parameter validation fails
     * @throws IOException
     *             if there are problems during conversion from json string to
     *             object.
     * @throws ResourceNotFoundException
     *             if the voter information is not found
     * @throws DuplicateEntryException
     *             if there is an error updating voting card state
     */
    @GET
    @Path("/tenant/{tenantId}/electionevent/{electionEventId}/votingcard/{votingCardId}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getChoiceCodes(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PARAMETER_VALUE_TENANT_ID) String tenantId,
            @PathParam(PARAMETER_VALUE_ELECTION_EVENT_ID) String electionEventId,
            @PathParam(PARAMETER_VALUE_VOTING_CARD_ID) String votingCardId,
            @NotNull @HeaderParam(PARAMETER_AUTHENTICATION_TOKEN) String authenticationTokenJsonString,
            @Context HttpServletRequest request)
            throws GeneralCryptoLibException, ApplicationException, IOException, ResourceNotFoundException,
            DuplicateEntryException, SemanticErrorException, SyntaxErrorException, VoteRepositoryException {

        // set the track id to be logged
        trackIdInstance.setTrackId(trackingId);

        // transaction id generation
        transactionInfoProvider.generate(tenantId, httpRequestService.getIpClientAddress(request),
            httpRequestService.getIpServer(request));

        // in case something from the input is not valid, it throws an exception
        validateInput(tenantId, electionEventId, votingCardId);

        AuthenticationToken authenticationToken;
        ValidationResult authTokenValidationResult;
        try {
            // convert from json to object
            authenticationToken = ObjectMappers.fromJson(authenticationTokenJsonString, AuthenticationToken.class);

            // validate auth token
            ValidationUtils.validate(authenticationToken);
            // returns the result of authentication token validation
            authTokenValidationResult = authenticationTokenService.validateAuthenticationToken(tenantId,
                electionEventId, votingCardId, authenticationTokenJsonString);
        } catch (IOException | IllegalArgumentException | ValidationException e) {
            LOG.error("Invalid authentication token format.", e);
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        // data to be logged
        secureLoggerInformation.setAuthenticationToken(authenticationTokenJsonString);
        secureLoggerInformation.setAuthenticationTokenObject(authenticationToken);

        // result of the operation
        ValidationVoteResult result = new ValidationVoteResult();

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

        // no need to continue if auth token is invalid
        if (!authTokenValidationResult.isResult()) {
            secureLoggerWriter.log(INFO, new LogContent.LogContentBuilder()
                .logEvent(VotingWorkflowLogEvents.STOP_VOTING_PROCESS)
                .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                .electionEvent(electionEventId)
                .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC, "Error validating authentication token")
                .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                .createLogInfo());
            return getResponseInvalidToken(authTokenValidationResult);
        }

        // result of operation
        com.scytl.products.ov.commons.validation.ValidationResult validationResult =
            validationRepository.validateElectionDatesInEI(tenantId, electionEventId,
                authenticationToken.getVoterInformation().getBallotBoxId());
        result.setValidationError(validationResult.getValidationError());

        if (!validationResult.isResult()) {
            JsonObject ballotBoxInformation = JsonUtils.getJsonObject(
                ballotBoxInformationRepository.getBallotBoxInfoByTenantIdElectionEventIdBallotBoxId(tenantId,
                    electionEventId, authenticationToken.getVoterInformation().getBallotBoxId()));
            String timeStampToken = authenticationToken.getTimestamp();
            secureLoggerWriter.log(INFO,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.CHOICE_CODES_ELECTION_OVER)
                    .objectId(secureLoggerInformation.getAuthenticationTokenHash())
                    .user(authenticationToken.getVoterInformation().getBallotBoxId()).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TIMESTAMP_AT, timeStampToken)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_ELECTION_DATE,
                        ballotBoxInformation.getString(JSON_PARAMETER_DATE_FROM) + DATE_SEPARATOR
                            + ballotBoxInformation.getString(JSON_PARAMETER_DATE_TO))
                    .createLogInfo());
            return Response.ok().entity(validationResult).build();
        }

        // check the voting card state
        // we may need to check the vc state consistency with the vote. It can
        // happen that we already confirmed the vote,
        // and so we're doing unnecessary work. since we're not changing
        // anything in the DB this is not problematic
        if (VotingCardStates.SENT_BUT_NOT_CAST.equals(votingCardState.getState())) {
            // recover json vote
            String authTokenHash = secureLoggerInformation.getAuthenticationTokenHash();

            // get the encrypted vote
            VoteAndComputeResults voteAndComputeResults;
            try {
                voteAndComputeResults =
                    voteRepository.findByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId, votingCardId);

                // validate vote
                ValidationUtils.validate(voteAndComputeResults);

                // data to log
                secureLoggerInformation.setVote(voteAndComputeResults.getVote());

                secureLoggerWriter.log(INFO, new LogContent.LogContentBuilder()
                    .logEvent(VotingWorkflowLogEvents.CHOICE_CODES_VOTE_FOUND).objectId(authTokenHash)
                    .user(votingCardId).electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_VOTE_HASH, secureLoggerInformation.getVoteHash())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_BALLOT_BOX_ID,
                        authenticationToken.getVoterInformation().getBallotBoxId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_DESC, "Vote found").createLogInfo());
            } catch (EJBException | ResourceNotFoundException | VoteRepositoryException e) {
                secureLoggerWriter.log(ERROR,
                    new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.CHOICE_CODES_VOTE_NOT_FOUND)
                        .objectId(authTokenHash).user(votingCardId).electionEvent(electionEventId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_BALLOT_BOX_ID,
                            authenticationToken.getVoterInformation().getBallotBoxId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC, "Vote not found").createLogInfo());
                throw e;
            }

            // generate choice codes
            try {
                ChoiceCodeAndComputeResults generateChoiceCodes =
                    choiceCodeRepository.generateChoiceCodes(tenantId, electionEventId,
                        authenticationToken.getVoterInformation().getVerificationCardId(), voteAndComputeResults);
                result.setChoiceCodes(generateChoiceCodes.getChoiceCodes());
                result.setValid(true);

                // sent choice code
                secureLoggerWriter.log(INFO,
                    new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.CHOICE_CODES_RETRIEVED)
                        .objectId(authTokenHash).user(votingCardId).electionEvent(electionEventId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_VERIFICATION_CARD_ID,
                            authenticationToken.getVoterInformation().getVerificationCardId())
                        .createLogInfo());
            } catch (EJBException | ResourceNotFoundException e) {
                // change status of voting card to CHOICE CODES FAILED
                votingCardStateService.updateVotingCardState(tenantId, electionEventId, votingCardId,
                    VotingCardStates.CHOICE_CODES_FAILED);
                result.setValid(Boolean.FALSE);
                result.setValidationError(new ValidationError(ValidationErrorType.FAILED));

                secureLoggerWriter.log(ERROR,
                    new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.CHOICE_CODES_ERROR_RETRIEVING)
                        .objectId(votingCardId).user(votingCardId).electionEvent(electionEventId)
                        .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_VERIFICATION_CARD_ID,
                            authenticationToken.getVoterInformation().getVerificationCardId())
                        .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC,
                            "Error calling to voting choice code generation services: "
                                + 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(votingCardId).user(votingCardId)
                .electionEvent(electionEventId)
                .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                .additionalInfo(VotingWorkflowLogConstants.INFO_ERR_DESC, "Wrong voting card state")
                .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                .createLogInfo());
        }

        // Returns authentication token
        return Response.ok().entity(result).build();
    }

    /**
     * Validate input.
     *
     * @param tenantId
     *            the tenant id
     * @param electionEventId
     *            the election event id
     * @param votingCardId
     *            the voting card id
     * @throws ApplicationException
     *             the application exception
     */
    // Does a basic validation of the input. In case something is wrong, just
    // throws an exception.
    private void validateInput(String tenantId, String electionEventId, String votingCardId)
            throws ApplicationException {
        if (tenantId == null || "".equals(tenantId)) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_TENANT_ID_IS_NULL);
        }
        if (electionEventId == null || "".equals(electionEventId)) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_ELECTION_EVENT_ID_IS_NULL);
        }
        if (votingCardId == null || "".equals(votingCardId)) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_VOTING_CARD_ID_IS_NULL);
        }
    }

    /**
     * Constructs the response given a invalid validation result
     *
     * @param validationResult
     * @return
     */
    private Response getResponseInvalidToken(ValidationResult validationResult) {

        Gson gson = new Gson();
        String json = gson.toJson(validationResult);
        return Response.status(Response.Status.UNAUTHORIZED).entity(json).build();

    }
}
