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

import java.io.IOException;
import java.io.OutputStream;
import java.util.Optional;

import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.json.JsonObject;
import javax.persistence.LockTimeoutException;
import javax.persistence.PessimisticLockException;

import org.apache.commons.io.output.CloseShieldOutputStream;
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.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.DuplicateEntryException;
import com.scytl.products.ov.commons.beans.exceptions.EntryPersistenceException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.exceptions.VoteCastCodeRepositoryException;
import com.scytl.products.ov.commons.beans.exceptions.VoteRepositoryException;
import com.scytl.products.ov.commons.logging.I18nLoggerMessages;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.util.JsonUtils;
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.common.csv.ExportedPartialVotingCardStateItemWriter;
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.confirmation.VoteCastCodeRepository;
import com.scytl.products.ov.vw.services.domain.model.information.VoterInformation;
import com.scytl.products.ov.vw.services.domain.model.information.VoterInformationRepository;
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.VotingCardStateRepository;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStates;
import com.scytl.products.ov.vw.services.domain.model.vote.VoteRepository;

/**
 * Service for handling voting card state.
 */
@Stateless
public class VotingCardStateServiceImpl implements VotingCardStateService {

    private static final I18nLoggerMessages I18N = I18nLoggerMessages.getInstance();

    /** The Constant INACTIVE_VOTING_CARDS_FILENAME. */
    private static final String CONFIRMATION_REQUIRED_ATTRIBUTE = "confirmationRequired";

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

    // An instance of voting card state repository
    @EJB
    private VotingCardStateRepository votingCardStateRepository;

    // The voter information Repository
    @EJB
    private VoterInformationRepository voterInformationRepository;

    /** The secure logger writer. */
    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    /** The track id instance. */
    @Inject
    private TrackIdInstance trackIdInstance;

    /** The Vote repository **/
    @EJB
    private VoteRepository voteRepository;

    @EJB
    private VoteCastCodeRepository voteCastCodeRepository;

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

    @EJB
    private BallotBoxInformationRepository ballotBoxInformationRepository;


    /**
     * @see com.scytl.products.ov.vw.services.domain.service.VotingCardStateService#initializeVotingCardState(com.scytl.products.ov.vw.services.domain.model.state.VotingCardState)
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void initializeVotingCardState(VotingCardState votingCardState)
            throws DuplicateEntryException, EntryPersistenceException, ResourceNotFoundException {
        if (votingCardState.getState().equals(VotingCardStates.NONE)) {
            votingCardState.setState(VotingCardStates.NOT_SENT);
        }

        final Optional<VotingCardState> votingCardState1 = votingCardStateRepository.acquire(
            votingCardState.getTenantId(), votingCardState.getElectionEventId(), votingCardState.getVotingCardId());
        // persist just in case that the voting card state is not already
        // persisted
        if (!votingCardState1.isPresent()) {
            votingCardStateRepository.save(votingCardState);
        }

    }

    /**
     * @see com.scytl.products.ov.vw.services.domain.service.VotingCardStateService#getVotingCardState(java.lang.String,
     *      java.lang.String, java.lang.String)
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public VotingCardState getVotingCardState(String tenantId, String electionEventId, String votingCardId)
            throws ApplicationException {
        // validate input parameters
        validateParameter(tenantId, "tenantId");
        validateParameter(electionEventId, "electionEventId");
        validateParameter(votingCardId, "votingCardId");

        // recover voting card state
        Optional<VotingCardState> votingCardState;
        try {
            votingCardState = votingCardStateRepository.acquire(tenantId, electionEventId, votingCardId);
        } catch (PessimisticLockException | LockTimeoutException e) {
            throw new ApplicationException("Cannot create lock on entity with id: " + votingCardId + " with tenant "
                + tenantId + " and electionEvent " + electionEventId, e);
        }

        if (!votingCardState.isPresent()) {
            VotingCardState state = new VotingCardState();
            state.setTenantId(tenantId);
            state.setElectionEventId(electionEventId);
            state.setVotingCardId(votingCardId);
            state.setState(VotingCardStates.NONE);
            votingCardState = Optional.of(state);
        } else {
            final VotingCardStates currentState = votingCardState.get().getState();
            final VotingCardStates realState =
                getConsistentVotingCardState(tenantId, electionEventId, votingCardId, currentState);
            votingCardState.get().setState(realState);
        }

        return votingCardState.get();
    }

    /**
     * @see com.scytl.products.ov.vw.services.domain.service.VotingCardStateService#updateVotingCardState(java.lang.String,
     *      java.lang.String, java.lang.String,
     *      com.scytl.products.ov.vw.services.domain.model.state.VotingCardStates)
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void updateVotingCardState(String tenantId, String electionEventId, String votingCardId,
            VotingCardStates state) throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        // validate input parameters
        validateParameter(tenantId, "tenantId");
        validateParameter(electionEventId, "electionEventId");
        validateParameter(votingCardId, "votingCardId");
        validateParameter(state, "votingCardState");

        // get the voting card state
        Optional<VotingCardState> optionalOfVotingCardState =
            votingCardStateRepository.acquire(tenantId, electionEventId, votingCardId);

        if (optionalOfVotingCardState.isPresent()) {
            // update state
            VotingCardState votingCardState = optionalOfVotingCardState.get();
            votingCardState.setState(state);

            votingCardStateRepository.save(votingCardState);

            LOG.info(I18N.getMessage("VotingCardStateService.updateVotingCardState.updateState"), tenantId,
                electionEventId, votingCardId, votingCardState.getState().name());
        } else {
            LOG.warn(I18N.getMessage("VotingCardStateService.getVotingCardState.notFound", tenantId, electionEventId,
                votingCardId));
        }
    }

    /**
     * @see com.scytl.products.ov.vw.services.domain.service.VotingCardStateService#updateVotingCardState(String,
     *      String, String, VotingCardStates)
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void blockVotingCardIgnoreUnable(String tenantId, String electionEventId, String votingCardId)
            throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        // validate input parameters
        validateParameter(tenantId, "tenantId");
        validateParameter(electionEventId, "electionEventId");
        validateParameter(votingCardId, "votingCardId");

        final VoterInformation voterInformation = getVoterInformation(tenantId, electionEventId, votingCardId);
        if (voterInformation == null) {
            return;
        }

        // get the voting card state
        VotingCardState votingCardState;
        try {
            final Optional<VotingCardState> current =
                votingCardStateRepository.acquire(tenantId, electionEventId, votingCardId);
            if (!current.isPresent()) {
                votingCardState = new VotingCardState();
                votingCardState.setTenantId(tenantId);
                votingCardState.setElectionEventId(electionEventId);
                votingCardState.setVotingCardId(votingCardId);
                votingCardState.setState(VotingCardStates.NONE);
            } else {
                votingCardState = current.get();
            }

        } catch (EJBException e) {
            LOG.error("Failed to acquire VotingCardState.", e);
            return;
        }

        VotingCardStateEvaluator stateEvaluator = VotingCardStateEvaluator.forState(votingCardState.getState());
        // check if associated ballotbox requires confirmation -> determines the
        // final state where we no longer can block
        // the voting card (yes->CAST, no->SENT_BUT_NOT_CAST)
        boolean confirmationRequired = doesVotingCardRequireConfirmation(voterInformation);
        if (stateEvaluator.canBeBlocked(confirmationRequired)) {
            // update state
            votingCardState.setState(VotingCardStates.BLOCKED);
            votingCardStateRepository.save(votingCardState);
            LOG.info(I18N.getMessage("VotingCardStateService.blockVotingCardIgnoreUnable.block"), tenantId,
                electionEventId, votingCardId);

            secureLoggerWriter.log(INFO,
                new LogContent.LogContentBuilder().logEvent(VotingWorkflowLogEvents.VOTING_CARD_BLOCKING_OK)
                    .objectId(votingCardId).user("admin").electionEvent(electionEventId)
                    .additionalInfo(VotingWorkflowLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .createLogInfo());
        } else {
            LOG.info(I18N.getMessage("VotingCardStateService.blockVotingCardIgnoreUnable.invalidState"),
                votingCardState.getState().name(), tenantId, electionEventId, votingCardId);
        }
    }

    /**
     * @see VotingCardStateService#incrementVotingCardAttempts(String, String,
     *      String)
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void incrementVotingCardAttempts(String tenantId, String electionEventId, String votingCardId)
            throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        // validate input parameters
        validateParameter(tenantId, "tenantId");
        validateParameter(electionEventId, "electionEventId");
        validateParameter(votingCardId, "votingCardId");

        // get the voting card state
        Optional<VotingCardState> optional =
            votingCardStateRepository.acquire(tenantId, electionEventId, votingCardId);

        if (optional.isPresent()) {
            // update state
            VotingCardState state = optional.get();
            state.incrementAttempts();
            votingCardStateRepository.save(state);

            LOG.info(I18N.getMessage("VotingCardStateService.updateVotingCardState.updateState"), tenantId,
                electionEventId, votingCardId, state.getState().name());
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void writeIdAndStateOfInactiveVotingCards(final String tenantId, final String electionEventId,
            final OutputStream stream) throws IOException {
        try (ExportedPartialVotingCardStateItemWriter writer =
            new ExportedPartialVotingCardStateItemWriter(new CloseShieldOutputStream(stream))) {
            votingCardStateRepository.findAndWriteVotingCardsWithInactiveState(tenantId, electionEventId, writer);
        }
    }

    public VotingCardStates getConsistentVotingCardState(final String tenantId, final String electionEventId,
            final String votingCardId, final VotingCardStates votingCardState) {

        // init with the 'current' state. we only change it if there is a
        // consistency mismatch
        VotingCardStates realVotingCardState = votingCardState;
        VotingCardStateEvaluator stateEvaluator = VotingCardStateEvaluator.forState(votingCardState);
        
        if (stateEvaluator.votingCardStateRequiresConsistencyCheck()) {
			boolean voteCastCodeExists;
			try {
				voteCastCodeExists = checkVoteCastCodeExists(tenantId, electionEventId, votingCardId);
				if (voteCastCodeExists) {
					realVotingCardState = VotingCardStates.CAST;
					LOG.info("There is a Vote Cast Code for this VotingCard [tenant=%s, election=%s, votingCard=%s]. "
							+ "Voting Card State '{}'", realVotingCardState.name());
				} else {
					// no vote cast code. check if a vote was sent
					final boolean voteExists = checkVoteExists(tenantId, electionEventId, votingCardId);
					if (voteExists) {
						realVotingCardState = VotingCardStates.SENT_BUT_NOT_CAST;
						LOG.info("There is a Vote for this VotingCard [tenant=%s, election=%s, votingCard=%s]. "
								+ "VotingCard state is '{}'", realVotingCardState.name());
					} else {
						realVotingCardState = VotingCardStates.NOT_SENT;
						LOG.info("There is no Vote for this VotingCard [tenant=%s, election=%s, votingCard=%s]."
								+ " VotingCard state is '{}'", realVotingCardState.name());
					}
				}
			} catch (VoteCastCodeRepositoryException vcE) {
				LOG.error("Error trying to check vote cast code exits.", vcE);
				realVotingCardState = VotingCardStates.CHOICE_CODES_FAILED;
			} catch (VoteRepositoryException vrE) {
				LOG.error("Error trying to check vote exits.", vrE);
				realVotingCardState = VotingCardStates.CHOICE_CODES_FAILED;
			}
        }
        return realVotingCardState;
    }

    private boolean checkVoteCastCodeExists(final String tenantId, final String electionEventId,
            final String votingCardId) throws VoteCastCodeRepositoryException {
        return voteCastCodeRepository.voteCastCodeExists(tenantId, electionEventId, votingCardId);
    }

    private boolean checkVoteExists(final String tenantId, final String electionEventId, final String votingCardId) throws VoteRepositoryException {
        return voteRepository.voteExists(tenantId, electionEventId, votingCardId);
    }

    /**
     * Check If Voting Card exists
     */
    private VoterInformation getVoterInformation(final String tenantId, final String electionEventId, String vcId) {

        try {
			return voterInformationRepository.getByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId, vcId);
        } catch (ResourceNotFoundException e) {
            LOG.error(
                I18N.getMessage("VotingCardStateService.getVoterInformation.notFound", tenantId, electionEventId, vcId),
                e);
            return null;
        }
    }

    private boolean doesVotingCardRequireConfirmation(final VoterInformation voterInformation) {
        String tenantId = voterInformation.getTenantId();
        String electionEventId = voterInformation.getElectionEventId();
        String ballotBoxId = voterInformation.getBallotBoxId();

        // get the ballot box the voting card/voterinformation is associated
        // with, and from there get the prop value.
        try {
            String ballotBoxJson = ballotBoxInformationRepository
                .getBallotBoxInfoByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId);
            JsonObject ballotBoxInformationJson = JsonUtils.getJsonObject(ballotBoxJson);
            return ballotBoxInformationJson.getBoolean(CONFIRMATION_REQUIRED_ATTRIBUTE);
        } catch (ResourceNotFoundException e) {
            LOG.info(I18N.getMessage("VotingCardStateService.getBallotBoxInformation.error", tenantId, electionEventId,
                ballotBoxId), e);
        }
        // if we can't get the information, return default value of
        // 'true(confirmation is required)'
        return true;
    }

    // Validates whether the parameter is null.
    private void validateParameter(Object value, String objectName) throws ApplicationException {
        if (value == null || "".equals(value)) {
            throw new ApplicationException(objectName + " is null or empty.");
        }
    }
}
