/*
 * 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 java.io.IOException;

import javax.ejb.EJB;
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 org.slf4j.LoggerFactory;

import com.google.gson.Gson;
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.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.validation.ValidationErrorType;
import com.scytl.products.ov.commons.dto.CastCodeAndComputeResults;
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.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.confirmation.VoteCastCodeRepository;
import com.scytl.products.ov.vw.services.domain.model.confirmation.VoteCastResult;
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.VoteCastCodeService;
import com.scytl.products.ov.vw.services.domain.service.VotingCardStateService;

/**
 * The end point for getting Cast Codes.
 */
@Path("/castcodes")
@Stateless(name = "vw-CastCodeResource")
public class CastCodeResource {

    // 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";
    
    // The cast code repository
    @EJB
    private VoteCastCodeRepository voteCastCodeRepository;
    
    // The voting card state service
    @EJB
    private VotingCardStateService votingCardStateService;
    
    // The vote repository
    @EJB
    private VoteRepository voteRepository;
    
    // The validation repository
    @EJB
    private ValidationRepository validationRepository;
    
    @Inject
    private VoteCastCodeService voteCastCodeService;
    
    // Tracking id used for logging and tracking purposes
    @Inject
    private TrackIdInstance trackIdInstance;
    
    // The ballot box information repository
    @EJB
    private BallotBoxInformationRepository ballotBoxInformationRepository;
    
    // The authentication token repository
    @Inject
    private AuthenticationTokenService authenticationTokenService;
    
    @Inject
    private TransactionInfoProvider transactionInfoProvider;
    
    @Inject
    private HttpRequestService httpRequestService;
    
    @Inject
    private SecureLoggingWriter secureLoggerWriter;
    
    @Inject
    private SecureLoggerInformation secureLoggerInformation;
    
    private static final Logger LOG = LoggerFactory.getLogger(CastCodeResource.class);
    
    /**
     * Gets the generate cast codes which matches with the given parameters and voting card state in CAST.
     *
     * @param tenantId                      - the tenant identifier.
     * @param electionEventId               - the election event identifier.
     * @param votingCardId                  - the voting card Identifier.
     * @param authenticationTokenJsonString - the authentication token in json format.
     * @param request                       - the http servlet request.
     * @return if the operation is successfully performed, the generated choice codes
     * @throws ApplicationException      if parameter validation fails
     * @throws ResourceNotFoundException if the voter information is not found
     * @throws GeneralCryptoLibException if length is out of the range for this generator during creation of token or in
     *                                   requestId generation.
     * @throws SemanticErrorException    if there are semantic problems during validation of input parameters.
     * @throws SyntaxErrorException      if there are syntax problems during validation of input parameters.
     * @throws DuplicateEntryException   if the unique constraint is violated.
     */
    @GET
    @Path("/tenant/{tenantId}/electionevent/{electionEventId}/votingcard/{votingCardId}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getCastCodeMessage(@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 ApplicationException, ResourceNotFoundException, GeneralCryptoLibException, IOException, DuplicateEntryException {

        // 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 operation
        ValidationVoteResult result = new ValidationVoteResult();

        // recover the voting card state (no need to check consistency with vote state)
        VotingCardState votingCardState =
            votingCardStateService.getVotingCardState(tenantId, electionEventId, votingCardId);


        //don't allow progressing if the auth token is invalid with an error code that IS NOT  "election over"
        if (!authTokenValidationResult.isResult() &&
            !ValidationErrorType.ELECTION_OVER_DATE.equals(authTokenValidationResult.getValidationError().getValidationErrorType())) {
            // no need to continue if auth token is invalid
            secureLoggerWriter.log(Level.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);
        }


        // check the voting card state
        if (VotingCardStates.CAST.equals(votingCardState.getState())) {
            // generate the cast code
            try {
                // validate
                com.scytl.products.ov.commons.validation.ValidationResult validationResult =
                    validationRepository.validateElectionDatesInEI(tenantId, electionEventId,
                        authenticationToken.getVoterInformation().getBallotBoxId());

                //if the election is over let the cast code be returned. otherwise fail.
                if (!validationResult.isResult()) {
                    JsonObject ballotBoxInformation = JsonUtils.getJsonObject(ballotBoxInformationRepository
                        .getBallotBoxInfoByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId,
                            authenticationToken.getVoterInformation().getBallotBoxId()));
                    String timeStampToken = authenticationToken.getTimestamp();

                    //in theory in this code block we can only get ELECTION_OVER_DATE or ELECTION_NOT_STARTED
                    if (ValidationErrorType.ELECTION_OVER_DATE
                        .equals(validationResult.getValidationError().getValidationErrorType())) {
                        secureLoggerWriter.log(Level.ERROR,
                            new LogContent.LogContentBuilder()
                                .logEvent(VotingWorkflowLogEvents.DB_CAST_CODE_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();

                    } else {
                        secureLoggerWriter.log(Level.ERROR,
                            new LogContent.LogContentBuilder()
                                .logEvent(VotingWorkflowLogEvents.DB_CAST_CODE_ELECTION_NOT_STARTED)
                                .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();
                    }
                }

                CastCodeAndComputeResults voteCastMessage =
                    voteCastCodeRepository.getCastCode(tenantId, electionEventId, votingCardId);

                VoteCastResult voteCastResult = voteCastCodeService.generateVoteCastResult(tenantId, electionEventId,
                    votingCardId, authenticationToken.getVoterInformation().getVerificationCardId(), voteCastMessage);

                // sent vote cast code
                secureLoggerWriter.log(Level.INFO, new LogContent.LogContentBuilder()
                    .logEvent(VotingWorkflowLogEvents.SENT_VOTE_CAST_CODE)
                    .objectId(secureLoggerInformation.getAuthenticationTokenHash()).user(votingCardId)
                    .electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                    .createLogInfo());

                return Response.ok().entity(voteCastResult).build();
            } catch (ResourceNotFoundException e) {
                secureLoggerWriter.log(Level.ERROR,
                    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 calling to voting cast code generation services: " +
                                ExceptionUtils.getRootCauseMessage(e))
                        .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                        .createLogInfo());
            }
        } else {
            secureLoggerWriter.log(Level.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, "Wrong voting card state")
                    .additionalInfo(VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD, votingCardState.getState().name())
                    .createLogInfo());
        }

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

    // 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();

    }

}
