/*
 * 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.channel.cc.orchestrator.mixdec.domain.services;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

import org.apache.commons.io.output.CloseShieldOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.products.ov.channel.cc.orchestrator.commons.config.QueuesConfig;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.MixDecExportedBallotBoxItemWriter;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.model.BallotBoxStatus;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.model.MixDecBallotBox;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.model.MixDecBallotBoxStatus;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.persistence.CleansedBallotBoxRepository;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.persistence.MixDecBallotBoxRepository;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.persistence.MixDecBallotBoxStatusRepository;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.persistence.MixDecNodeOutputRepository;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.remote.ResourceNotReadyException;
import com.scytl.products.ov.commons.beans.domain.model.EntityId;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxId;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxIdImpl;
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.dto.MixingDTO;
import com.scytl.products.ov.commons.dto.MixingDTOImpl;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.errormanagement.ExecutionPolicy;
import com.scytl.products.ov.commons.messaging.MessagingException;
import com.scytl.products.ov.commons.messaging.MessagingService;
import com.scytl.products.ov.commons.messaging.Queue;

@Stateless
public class MixDecBallotBoxServiceImpl implements MixDecBallotBoxService {

    private static final int PAGE_SIZE = 500;

    @Inject
    private MixDecBallotBoxRepository mixDecBallotBoxRepository;

    @Inject
    private MixDecBallotBoxStatusRepository mixDecBallotBoxStatusRepository;

    @Inject
    private MixDecNodeOutputRepository nodeOutputRepository;

    @Inject
    private CleansedBallotBoxRepository cleansedBallotBoxRepository;

    @Inject
    private MessagingService messagingService;

    @Inject
    private MixDecMixingDecryptionConsumer mixDecMixingDecryptionConsumer;

    @Inject
    private ExecutionPolicy executionPolicy;

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

    @Override
    public void startup() throws MessagingException {
        LOG.info("OR - Setting up consumers for the mixing and decryption queues...");
        Queue[] queues = QueuesConfig.MIX_DEC_COMPUTATION_RES_QUEUES;

        if (queues == null || queues.length == 0) {
            throw new IllegalStateException("No mixing+decryption response queues provided to listen to.");
        }

        try {
            for (Queue queue : queues) {
                messagingService.createReceiver(queue, mixDecMixingDecryptionConsumer);
                LOG.info("Mixing and decryption queue consumer for {} is up and running", queue.name());
            }
        } catch (Exception e) {
            throw e instanceof MessagingException ? (MessagingException) e : new MessagingException(e);
        }
    }

    @Override
    public void shutdown() throws MessagingException {
        LOG.info("OR - Tearing down consumers for the mixing and decryption queues...");
        Queue[] queues = QueuesConfig.MIX_DEC_COMPUTATION_RES_QUEUES;

        if (queues == null || queues.length == 0) {
            throw new IllegalStateException("No mixing+decryption response queues provided to listen to.");
        }

        for (Queue queue : queues) {
            messagingService.destroyReceiver(queue, mixDecMixingDecryptionConsumer);
        }

        LOG.info("OR - Mixing and decryption queue consumers torn down for {}", (Object[]) queues);
    }

    @Override
    public List<BallotBoxStatus> processBallotBoxes(final String tenantId, final String electionEventId,
            List<EntityId> ballotBoxIds, String trackingId) {
        List<BallotBoxStatus> ballotBoxStatuses = new ArrayList<>();
        LOG.debug("Received list of ballot boxes for mixing and decrypting:",
            ballotBoxIds.stream().map(EntityId::getId).collect(Collectors.joining(", ")));

        for (EntityId entityId : ballotBoxIds) {
            String ballotBoxId = entityId.getId();
            LOG.info("Ballot box {} is about to be processed", ballotBoxId);

            BallotBoxStatus ballotBoxStatus = new BallotBoxStatus();
            ballotBoxStatus.setBallotBoxId(ballotBoxId);

            if (isBallotBoxMixingStarted(tenantId, electionEventId, ballotBoxId)) {
                ballotBoxStatus.setProcessStatus(BallotBoxStatus.STATUS_ERROR);
                ballotBoxStatus.setErrorMessage("Ballot box mixing has already started");
                ballotBoxStatuses.add(ballotBoxStatus);
                continue;
            }

            try {
                // Set the ballot box to CLOSED status to signal the start of
                // the mixing.
                ballotBoxStatus.setProcessStatus(BallotBoxStatus.STATUS_CLOSED);
                saveStatus(tenantId, electionEventId, ballotBoxId, ballotBoxStatus.getProcessStatus());

                LOG.info("Starting processing of ballot box {}", ballotBoxId);
                processBallotBox(tenantId, electionEventId, ballotBoxId, trackingId);
            } catch (ResourceNotReadyException e) {
                LOG.info("Ballot box not ready for mixdecrypt", e);
                ballotBoxStatus.setProcessStatus(BallotBoxStatus.STATUS_NOT_CLOSED);
            } catch (ResourceNotFoundException e) {
                LOG.warn("Could not find requested ballot box", e);
                ballotBoxStatus.setProcessStatus(BallotBoxStatus.STATUS_NOT_FOUND);
            } catch (Exception e) {
                LOG.error("Error processing ballotbox", e);
                ballotBoxStatus.setProcessStatus(BallotBoxStatus.STATUS_ERROR);
                ballotBoxStatus.setErrorMessage(e.toString());
            } finally {
                if (!ballotBoxStatus.getProcessStatus().equals(BallotBoxStatus.STATUS_CLOSED)) {
                    updateStatus(tenantId, electionEventId, ballotBoxId, ballotBoxStatus.getProcessStatus(),
                        ballotBoxStatus.getErrorMessage());
                }
            }

            LOG.info("Ballot box {} status is now {} ", ballotBoxStatus.getBallotBoxId(),
                ballotBoxStatus.getProcessStatus());

            ballotBoxStatuses.add(ballotBoxStatus);
        }

        return ballotBoxStatuses;
    }

    /**
     * Saves the ballot box status, eventually replacing a previous status.
     *
     * @param tenantId
     *            the tenant
     * @param electionEventId
     *            the election event
     * @param ballotBoxId
     *            the ballot box
     * @param processStatus
     *            the status
     */
    private void saveStatus(String tenantId, String electionEventId, String ballotBoxId, String processStatus)
            throws DuplicateEntryException, EntryPersistenceException {
        try {
            mixDecBallotBoxStatusRepository.getMixDecBallotBoxStatus(tenantId, electionEventId, ballotBoxId);
            mixDecBallotBoxStatusRepository.update(tenantId, electionEventId, ballotBoxId, processStatus, null);
        } catch (ResourceNotFoundException e) {
            mixDecBallotBoxStatusRepository.save(tenantId, electionEventId, ballotBoxId, processStatus, null);
        }
    }

    private boolean isBallotBoxMixingStarted(String tenantId, String electionEventId, String ballotBoxId) {
        try {
            MixDecBallotBoxStatus mixDecBallotBoxStatus =
                mixDecBallotBoxStatusRepository.getMixDecBallotBoxStatus(tenantId, electionEventId, ballotBoxId);
            return BallotBoxStatus.STATUS_MIXED.equals(mixDecBallotBoxStatus.getStatus())
                || BallotBoxStatus.STATUS_CLOSED.equals(mixDecBallotBoxStatus.getStatus());
        } catch (ResourceNotFoundException e) {
            LOG.debug("Ballot box " + ballotBoxId + " mixing status not found", e);
            return false;
        }
    }

    @Override
    public void process(MixingDTO mixingDTO) throws TimeoutException, ApplicationException {
        try {
            // Send the mixing data to a node.
            String queueName = mixingDTO.getQueuesToVisit().iterator().next();
            LOG.info("Sending mixing DTO {} to queue {} for mixing", mixingDTO.getPayload().getVoteSetId(), queueName);
            Queue queue = new Queue(queueName);
            try {
                messagingService.send(queue, mixingDTO);
            } catch (Throwable e) {
                throw e instanceof MessagingException ? (MessagingException) e : new MessagingException(e);
            }
        } catch (MessagingException e) {
            LOG.error("Failed to publish the mixing-and-decryption request to the queues: " + e.getMessage());
            throw new ApplicationException(e);
        }
    }

    private void processBallotBox(final String tenantId, final String electionEventId, final String ballotBoxId,
            final String trackingId)
            throws ResourceNotReadyException, ResourceNotFoundException, IOException {

        // Start by requiring a visit to each node.
        Set<String> nodesToVisit =
            Stream.of(QueuesConfig.MIX_DEC_COMPUTATION_REQ_QUEUES).map(Queue::name).collect(Collectors.toSet());

        final BallotBoxId bbid = new BallotBoxIdImpl(tenantId, electionEventId, ballotBoxId);
        // Create a new mixing DTO.
        cleansedBallotBoxRepository.getMixingPayloads(bbid).forEach(payload -> {
            // Create a DTO to wrap the payload.
            if (null != payload) {
                try {
                    process(new MixingDTOImpl(trackingId, nodesToVisit, payload));
                } catch (TimeoutException toE) {
                    LOG.error("Mixing payload {} delivery timed out", payload.getVoteSetId(), toE);
                } catch (ApplicationException aE) {
                    LOG.error("Error mixing payload {}.", payload.getVoteSetId(), aE);
                }
            }
        });
    }

    @Override
    /**
     * @deprecated (/ / SV - 5291)
     */
    @Deprecated // SV-5291
    public List<MixDecBallotBox> getProcessedBallotBox(String tenantId, String electionEventId, String ballotBoxId) {
        return mixDecBallotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId,
            ballotBoxId);
    }

    @Override
    public void writeEncryptedBallotBox(final OutputStream stream, final String tenantId, final String electionEventId,
            final String ballotBoxId)
            throws IOException {
        LOG.info("Retrieving mixdec ballot box box {} for tenant {} and election event {}.", ballotBoxId, tenantId,
            electionEventId);
        writeMixDecBallotBoxItems(stream, tenantId, electionEventId, ballotBoxId);
    }

    private void writeMixDecBallotBoxItems(final OutputStream stream, final String tenantId,
            final String electionEventId, final String ballotBoxId)
            throws IOException {
        try (MixDecExportedBallotBoxItemWriter writer =
            new MixDecExportedBallotBoxItemWriter(new CloseShieldOutputStream(stream))) {

            int start = 0;

            List<MixDecBallotBox> mixDecBallotBox = mixDecBallotBoxRepository
                .findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId, start, PAGE_SIZE);

            while (!mixDecBallotBox.isEmpty()) {
                for (MixDecBallotBox item : mixDecBallotBox) {
                    writer.write(item.getVote());
                }

                start += PAGE_SIZE;

                mixDecBallotBox = mixDecBallotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId,
                    electionEventId, ballotBoxId, start, PAGE_SIZE);
            }

        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void addProcessedBallotBoxWithStatus(MixingPayload payload) {
        if (null == payload.getShuffledVotes()) {
            throw new IllegalArgumentException("No shuffled votes available. Is this an input DTO?");
        }

        BallotBoxId bbid = payload.getVoteSetId().getBallotBoxId();
        final String tenantId = bbid.getTenantId();
        final String electionEventId = bbid.getElectionEventId();
        final String ballotBoxId = bbid.getId();

        // Save each vote.
        payload.getVotes().forEach(vote -> {
            try {
                mixDecBallotBoxRepository.save(tenantId, electionEventId, ballotBoxId, vote.toString());
            } catch (DuplicateEntryException e) {
                LOG.warn("A vote in the ballot box {} (tenant {}, election event {}) is already stored: {}",
                    ballotBoxId, tenantId, electionEventId, vote, e);
            }
        });

        updateStatus(tenantId, electionEventId, ballotBoxId, BallotBoxStatus.STATUS_MIXED);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void addMixingErrorStatus(String tenantId, String electionEventId, String ballotBoxId, String errorMessage) {
        updateStatus(tenantId, electionEventId, ballotBoxId, BallotBoxStatus.STATUS_ERROR, errorMessage);
    }

    private void updateStatus(String tenantId, String electionEventId, String ballotBoxId, String ballotBoxStatus) {
        updateStatus(tenantId, electionEventId, ballotBoxId, ballotBoxStatus, null);
    }

    private void updateStatus(String tenantId, String electionEventId, String ballotBoxId, String ballotBoxStatus,
            String errorMessage) {
        try {
            mixDecBallotBoxStatusRepository.update(tenantId, electionEventId, ballotBoxId, ballotBoxStatus,
                errorMessage);
        } catch (ResourceNotFoundException | EntryPersistenceException e) {
            throw new IllegalStateException("Failed to update Ballot Box mixing status", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private void addProcessedBallotBox(String tenantId, String electionEventId, String ballotBoxId,
            String encryptedVote) {

        try {
            mixDecBallotBoxRepository.save(tenantId, electionEventId, ballotBoxId, encryptedVote);
        } catch (DuplicateEntryException e) {
            LOG.warn("Trying to store a mixed ballot box more than once ", e);
        }
    }

    @Override
    public void addProcessingBallotBoxError(String tenantId, String electionEventId, String ballotBoxId,
            String errorMessage) {
        try {
            mixDecBallotBoxStatusRepository.update(tenantId, electionEventId, ballotBoxId, BallotBoxStatus.STATUS_ERROR,
                errorMessage);
        } catch (EntryPersistenceException | ResourceNotFoundException e) {
            throw new IllegalStateException("Failed to update Ballot Box mixing status", e);
        }
    }

    @Override
    public List<BallotBoxStatus> getMixDecBallotBoxStatus(String tenantId, String electionEventId,
            String[] ballotBoxIds) {

        List<BallotBoxStatus> ballotBoxStatusList = new ArrayList<>();

        for (String ballotBoxId : ballotBoxIds) {
            BallotBoxStatus ballotBoxStatus = new BallotBoxStatus();
            ballotBoxStatus.setBallotBoxId(ballotBoxId);

            try {
                MixDecBallotBoxStatus mixDecBallotBoxStatus =
                    mixDecBallotBoxStatusRepository.getMixDecBallotBoxStatus(tenantId, electionEventId, ballotBoxId);

                ballotBoxStatus.setProcessStatus(mixDecBallotBoxStatus.getStatus());
                ballotBoxStatus.setErrorMessage(mixDecBallotBoxStatus.getErrorMessage());
            } catch (ResourceNotFoundException ignored) {
                LOG.debug("Resource not found ignored.", ignored);
                ballotBoxStatus.setProcessStatus(BallotBoxStatus.STATUS_NOT_FOUND);
            }

            ballotBoxStatusList.add(ballotBoxStatus);
        }

        return ballotBoxStatusList;
    }

    @Override
    public Stream<PayloadJson> getOutputPayloadsJson(BallotBoxId ballotBoxId) {
        return nodeOutputRepository
            .findForAllNodes(ballotBoxId.getTenantId(), ballotBoxId.getElectionEventId(), ballotBoxId.getId()).stream()
            .map(nodeOutput -> new PayloadJson(ballotBoxId, nodeOutput.getNodeName(), nodeOutput.getVoteSetIndex(),
                nodeOutput.getPayloadJson()));
    }
}
