/*
 * 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.json.JsonException;
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.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.dto.CastCodeAndComputeResults;
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.ValidationError;
import com.scytl.products.ov.vw.infrastructure.log.ConfirmationMessageHashService;
import com.scytl.products.ov.vw.services.domain.model.authentication.AuthenticationToken;
import com.scytl.products.ov.vw.services.domain.model.confirmation.ConfirmationInformation;
import com.scytl.products.ov.vw.services.domain.model.confirmation.ConfirmationInformationRepository;
import com.scytl.products.ov.vw.services.domain.model.confirmation.ConfirmationInformationResult;
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.information.VoterInformation;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardState;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStates;
import static com.scytl.products.oscore.logging.api.domain.Level.ERROR;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.CONFIRMATION_ATTEMPTS;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_BALLOT_BOX_ID;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_ERR_DESC;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_HASH_CONFIRMATION_MESSAGE;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_TRACK_ID;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.CONFIRMATION_MESSAGE_VALIDATION_INCREASE_ATTEMPTS;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.CONFIRMATION_MESSAGE_VALIDATION_INVALID_VOTING_CARD_STATE;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.CONFIRMATION_MESSAGE_VALIDATION_MAX_ATTEMPTS;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.STOP_VOTING_PROCESS;
import static java.text.MessageFormat.format;

/**
 * Implementation of {@link CastVoteService} as a stateless session bean.
 */
@Stateless
public class CastVoteServiceImpl implements CastVoteService {
    @Inject
    private VotingCardStateService stateService;

    @Inject
    private VoteCastCodeRepository codeRepository;

    @Inject
    private VoteCastCodeService codeService;

    @Inject
    private ConfirmationInformationRepository confirmationRepository;

    @Inject
    private TrackIdInstance trackIdInstance;

    @Inject
    private AuthTokenHashService tokenHashService;

    @Inject
    private ConfirmationMessageHashService confirmationHashService;

    @Inject
    private SecureLoggingWriter secureLogger;

    private static final Logger LOG = LoggerFactory.getLogger("std");
    
    @Override
    public VoteCastResult castVote(final String authenticationToken, final VoterInformation voter,
            final ConfirmationInformation confirmation) throws ResourceNotFoundException {
        VoteCastResult result;
        try {
            result = doCastVote(authenticationToken, voter, confirmation);
        } catch (WrongBallotCastingKeyException e) {
            result = handleWrongBallotCastingKey(authenticationToken, voter, confirmation, e.getState());
            LOG.debug("Failed to cast vote.", e);
        } catch (NumberOfAttemptsExceededException e) {
            result = handleNumberOfAttemtsExceeded(authenticationToken, voter, confirmation);
            LOG.debug("Failed to cast vote.", e);
        } catch (IllegalVotingCardStateException e) {
            result = handleIllegalVotingCardState(authenticationToken, voter, e.getState());
            LOG.debug("Failed to cast vote.", e);
        }
        return result;
    }

    private CastCodeAndComputeResults createVoteCastMessage(final String token, final VoterInformation voter,
            final ConfirmationInformation confirmation, final VotingCardState state)
            throws WrongBallotCastingKeyException, NumberOfAttemptsExceededException {
        CastCodeAndComputeResults message = generateVoteCastMessage(token, voter, confirmation, state);
        storeVoteCastMessage(token, state, message);
        return message;
    }

    private VoteCastResult doCastVote(final String token, final VoterInformation voter,
            final ConfirmationInformation confirmation)
            throws WrongBallotCastingKeyException, NumberOfAttemptsExceededException, IllegalVotingCardStateException,
            ResourceNotFoundException {
        VotingCardState state = getVotingCardState(voter);
        validateVotingCardState(state);
        validateConfirmation(token, confirmation, state);
        incrementNumberOfAttempts(state);
        CastCodeAndComputeResults message = createVoteCastMessage(token, voter, confirmation, state);
        setVotingCardState(state, VotingCardStates.CAST);
        return generateVoteCastResult(voter, state, message);
    }

    private CastCodeAndComputeResults generateVoteCastMessage(String authenticationToken, final VoterInformation voter,
            final ConfirmationInformation confirmation, final VotingCardState state)
            throws WrongBallotCastingKeyException, NumberOfAttemptsExceededException {
        CastCodeAndComputeResults message;
        try {
        	AuthenticationToken authenticationTokenObject = ObjectMappers.fromJson(authenticationToken,
                    AuthenticationToken.class);
            message = codeRepository.generateCastCode(state.getTenantId(), state.getElectionEventId(),
                voter.getVerificationCardId(), voter.getVotingCardId(), authenticationTokenObject.getSignature(), confirmation.getConfirmationMessage());
        } catch (CryptographicOperationException | IOException e) {
            if (state.hasRemainingAttempts()) {
                throw new WrongBallotCastingKeyException(state, e);
            } else {
                setVotingCardState(state, VotingCardStates.WRONG_BALLOT_CASTING_KEY);
                throw new NumberOfAttemptsExceededException(state, e);
            }
        }
        return message;
    }

    private VoteCastResult generateVoteCastResult(final VoterInformation voter, final VotingCardState state,
            final CastCodeAndComputeResults message) {
        VoteCastResult result;
        try {
            result = codeService.generateVoteCastResult(state.getTenantId(), state.getElectionEventId(),
                state.getVotingCardId(), voter.getVerificationCardId(), message);
        } catch (ResourceNotFoundException | IOException e) {
            throw new EJBException(
                format("Failed to generate vote cast result for voting card ''{0}.''.", state.getVotingCardId()), e);
        }
        return result;
    }

    private String getAuthenticationTokenHash(final String token) {
        String hash = "";
        try {
            hash = tokenHashService.hash(JsonUtils.getJsonObject(token));
        } catch (JsonException | GeneralCryptoLibException e) {
            LOG.error("Failed to get hash of authntication token.", e);
        }
        return hash;
    }

    private String getConfirmationInformationHash(final ConfirmationInformation confirmation) {
        return confirmationHashService.hash(confirmation);
    }

    private VotingCardState getVotingCardState(final VoterInformation voter) {
        VotingCardState state;
        try {
            state = stateService.getVotingCardState(voter.getTenantId(), voter.getElectionEventId(),
                voter.getVotingCardId());
        } catch (ApplicationException e) {
            throw new EJBException(format("Failed to get voting card ''{0}''.", voter.getVotingCardId()), e);
        }
        return state;
    }

    private VoteCastResult handleIllegalVotingCardState(final String token, final VoterInformation voter,
            final VotingCardState state) {
        String tokenHash = getAuthenticationTokenHash(token);
        String votingCardId = voter.getVotingCardId();
        String electionEventId = voter.getElectionEventId();
        String status = state.getState().name();
        secureLogger.log(ERROR,
            new LogContent.LogContentBuilder().logEvent(CONFIRMATION_MESSAGE_VALIDATION_INVALID_VOTING_CARD_STATE)
                .objectId(tokenHash).user(votingCardId).electionEvent(electionEventId)
                .additionalInfo(INFO_TRACK_ID, trackIdInstance.getTrackId())
                .additionalInfo(INFO_ERR_DESC, "Wrong voting card state")
                .additionalInfo(INFO_STATUS_VOTING_CARD, status).createLogInfo());
        VoteCastResult result = new VoteCastResult();
        result.setValid(false);
        result.setElectionEventId(voter.getElectionEventId());
        result.setVerificationCardId(voter.getVerificationCardId());
        result.setVotingCardId(voter.getVotingCardId());
        return result;
    }

    private VoteCastResult handleNumberOfAttemtsExceeded(final String token, final VoterInformation voter,
            final ConfirmationInformation confirmation) {
        String tokenHash = getAuthenticationTokenHash(token);
        String votingCardId = voter.getVotingCardId();
        String electionEventId = voter.getElectionEventId();
        String confirmationHash = getConfirmationInformationHash(confirmation);
        String ballotBoxId = voter.getBallotBoxId();
        secureLogger.log(ERROR,
            new LogContent.LogContentBuilder().logEvent(CONFIRMATION_MESSAGE_VALIDATION_MAX_ATTEMPTS)
                .objectId(tokenHash).user(votingCardId).electionEvent(electionEventId)
                .additionalInfo(INFO_TRACK_ID, trackIdInstance.getTrackId())
                .additionalInfo(INFO_HASH_CONFIRMATION_MESSAGE, confirmationHash)
                .additionalInfo(INFO_BALLOT_BOX_ID, ballotBoxId)
                .additionalInfo(INFO_ERR_DESC, "Max number of attempts reached").createLogInfo());
        VoteCastResult result = new VoteCastResult();
        result.setValid(false);
        ValidationError error = new ValidationError(ValidationErrorType.BCK_ATTEMPTS_EXCEEDED);
        result.setValidationError(error);
        result.setElectionEventId(voter.getElectionEventId());
        result.setVerificationCardId(voter.getVerificationCardId());
        result.setVotingCardId(voter.getVotingCardId());
        return result;
    }

    private VoteCastResult handleWrongBallotCastingKey(final String token, final VoterInformation voter,
            final ConfirmationInformation confirmation, final VotingCardState state) {
        String tokenHash = getAuthenticationTokenHash(token);
        String votingCardId = voter.getVotingCardId();
        String electionEventId = voter.getElectionEventId();
        String confirmationHash = getConfirmationInformationHash(confirmation);
        String remainingAttempts = Long.toString(state.getRemainingAttempts());
        secureLogger.log(Level.WARN,
            new LogContent.LogContentBuilder().logEvent(CONFIRMATION_MESSAGE_VALIDATION_INCREASE_ATTEMPTS)
                .objectId(tokenHash).user(votingCardId).electionEvent(electionEventId)
                .additionalInfo(INFO_TRACK_ID, trackIdInstance.getTrackId())
                .additionalInfo(INFO_HASH_CONFIRMATION_MESSAGE, confirmationHash)
                .additionalInfo(CONFIRMATION_ATTEMPTS, remainingAttempts)
                .additionalInfo(INFO_ERR_DESC, "Wrong ballot casting key").createLogInfo());
        VoteCastResult result = new VoteCastResult();
        result.setValid(false);
        ValidationError validationError = new ValidationError(ValidationErrorType.WRONG_BALLOT_CASTING_KEY);
        String[] errorArgs = {remainingAttempts };
        validationError.setErrorArgs(errorArgs);
        result.setValidationError(validationError);
        result.setElectionEventId(voter.getElectionEventId());
        result.setVerificationCardId(voter.getVerificationCardId());
        result.setVotingCardId(voter.getVotingCardId());
        return result;
    }

    private void incrementNumberOfAttempts(final VotingCardState state) {
        try {
            stateService.incrementVotingCardAttempts(state.getTenantId(), state.getElectionEventId(),
                state.getVotingCardId());
        } catch (ResourceNotFoundException | ApplicationException | DuplicateEntryException e) {
            throw new EJBException(
                format("Failed to increment the number of attempts of voting card ''{0}''.", state.getVotingCardId()),
                e);
        }
    }

    private void setVotingCardState(final VotingCardState state, final VotingCardStates value) {
        try {
            stateService.updateVotingCardState(state.getTenantId(), state.getElectionEventId(), state.getVotingCardId(),
                value);
        } catch (ResourceNotFoundException | ApplicationException | DuplicateEntryException e) {
            throw new EJBException(
                format("Failed to set the state of voting card ''{0}'' to ''{1}''.", state.getVotingCardId(), value),
                e);
        }
        state.setState(value);
    }

    private void storeVoteCastMessage(final String token, final VotingCardState state, final CastCodeAndComputeResults message) {
        String electionEventId = state.getElectionEventId();
        String votingCardId = state.getVotingCardId();
        boolean stored;
        try {
            stored = codeRepository.storesCastCode(state.getTenantId(), electionEventId, votingCardId, message);
        } catch (ResourceNotFoundException e) {
            throw new EJBException(format("Failed to store vote cast code for voting card ''{0}''.", votingCardId), e);
        }
        if (!stored) {
            String tokenHash = getAuthenticationTokenHash(token);
            String status = state.getState().name();
            secureLogger.log(ERROR,
                new LogContent.LogContentBuilder().logEvent(STOP_VOTING_PROCESS).objectId(tokenHash).user(votingCardId)
                    .electionEvent(electionEventId).additionalInfo(INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(INFO_ERR_DESC, "Error storing cast code")
                    .additionalInfo(INFO_STATUS_VOTING_CARD, status).createLogInfo());
            throw new EJBException(format("Failed to store vote cast code for voting card ''{0}''.", votingCardId));
        }
    }

    private void validateConfirmation(final String token, final ConfirmationInformation confirmation,
            final VotingCardState state)
            throws WrongBallotCastingKeyException, NumberOfAttemptsExceededException, ResourceNotFoundException {
        ConfirmationInformationResult result = confirmationRepository.validateConfirmationMessage(state.getTenantId(),
            state.getElectionEventId(), state.getVotingCardId(), confirmation, token);
        if (!result.isValid()) {
            throw new WrongBallotCastingKeyException(state);
        }
    }

    private void validateVotingCardState(final VotingCardState state)
            throws NumberOfAttemptsExceededException, IllegalVotingCardStateException {
        switch (state.getState()) {
        case SENT_BUT_NOT_CAST:
            break;
        case WRONG_BALLOT_CASTING_KEY:
            throw new NumberOfAttemptsExceededException(state);
        default:
            throw new IllegalVotingCardStateException(state);
        }
    }

    private static class CastVoteException extends Exception {
        private static final long serialVersionUID = -6245489356768280082L;

        private final transient VotingCardState state;

        public CastVoteException(final VotingCardState state) {
            this.state = state;
        }

        public CastVoteException(final VotingCardState state, final Throwable throwable) {
            super(throwable);
            this.state = state;
        }

        public VotingCardState getState() {
            return state;
        }

    }

    private static class IllegalVotingCardStateException extends CastVoteException {
        private static final long serialVersionUID = 1022274489710389341L;

        public IllegalVotingCardStateException(final VotingCardState state) {
            super(state);
        }
    }

    private static class NumberOfAttemptsExceededException extends CastVoteException {
        private static final long serialVersionUID = 932620521268368648L;

        public NumberOfAttemptsExceededException(final VotingCardState state) {
            super(state);
        }

        public NumberOfAttemptsExceededException(final VotingCardState state, final Throwable throwable) {
            super(state, throwable);
        }
    }

    private static class WrongBallotCastingKeyException extends CastVoteException {
        private static final long serialVersionUID = 6533401291620561667L;

        public WrongBallotCastingKeyException(final VotingCardState state) {
            super(state);
        }

        public WrongBallotCastingKeyException(final VotingCardState state, final Throwable throwable) {
            super(state, throwable);
        }
    }
}
