/*
 * 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.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObjectBuilder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.channel.cc.orchestrator.commons.config.QueuesConfig;
import com.scytl.products.ov.channel.cc.orchestrator.commons.config.TopicsConfig;
import com.scytl.products.ov.channel.cc.orchestrator.commons.polling.PollingService;
import com.scytl.products.ov.commons.beans.domain.model.messaging.SafeStreamDeserializationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.dto.CCPublicKey;
import com.scytl.products.ov.commons.dto.KeyCreationDTO;
import com.scytl.products.ov.commons.messaging.DestinationNotFoundException;
import com.scytl.products.ov.commons.messaging.MessageListener;
import com.scytl.products.ov.commons.messaging.MessagingException;
import com.scytl.products.ov.commons.messaging.MessagingService;
import com.scytl.products.ov.commons.messaging.Queue;
import com.scytl.products.ov.commons.safestream.StreamSerializableObjectReaderImpl;

public class MixDecKeyGenerationServiceImpl implements MixDecKeyGenerationService {
    private static final Logger LOG = LoggerFactory.getLogger("std");
    
    private static final int INDEX_OF_KEY_WITHIN_GENERATED_LIST = 0;

    @Inject
    private MessagingService messagingService;

    @Inject
    @MixDecKeyGeneration
    @Contributions
    private MessageListener contributionsConsumer;

    @Inject
    @MixDecKeyGeneration
    @ResultsReady
    private MessageListener resultsReadyConsumer;

    @Inject
    @MixDecKeyGeneration
    private PollingService<List<byte[]>> pollingService;

    @Override
    public Map<String, List<String>> requestMixDecKeyGenerationSync(String trackingId, String tenantId,
            String electionEventId, final List<String> electoralAuthorityIds, ZonedDateTime keysDateFrom,
            ZonedDateTime keysDateTo, ElGamalEncryptionParameters elGamalEncryptionParameters)
            throws IOException, ResourceNotFoundException, GeneralCryptoLibException {

        LOG.info("OR - Requesting MixDec keys");

        Map<String, List<String>> electoralAuthorityKeys = new HashMap<>();

        for (String electoralAuthorityId : electoralAuthorityIds) {

            KeyCreationDTO keyCreationDTO = new KeyCreationDTO();
            keyCreationDTO.setCorrelationId(UUID.randomUUID());
            keyCreationDTO.setElectionEventId(electionEventId);
            keyCreationDTO.setResourceId(electoralAuthorityId);
            keyCreationDTO.setFrom(keysDateFrom);
            keyCreationDTO.setTo(keysDateTo);
            keyCreationDTO.setEncryptionParameters(elGamalEncryptionParameters.toJson());
            keyCreationDTO.setRequestId(trackingId);

            publishToReqQ(keyCreationDTO);

            List<KeyCreationDTO> collectedMessages = collectKeyCreationDTOMessages(keyCreationDTO);

            List<String> collectedPublicKeys = collectPublicKeys(collectedMessages);

            electoralAuthorityKeys.put(electoralAuthorityId, collectedPublicKeys);
        }

        return electoralAuthorityKeys;
    }

    @Override
    public void startup() throws MessagingException {
        LOG.info("OR - Consuming the partial results ready notification topic");
        consumeResultsReady();
        LOG.info("OR - Consuming the MixDec key generation queue");
        consumeKeyGenerationContributions();
    }

    private void consumeKeyGenerationContributions()
            throws DestinationNotFoundException, MessagingException {
        Queue[] queues = QueuesConfig.MIX_DEC_KEY_GENERATION_RES_QUEUES;
        
        if (queues == null || queues.length == 0) {
            throw new IllegalStateException("No MixDec key generation response queues provided to listen to.");
        }
        
        for (Queue queue : queues) {
            messagingService.createReceiver(queue, contributionsConsumer);
        }
    }
    
    private void consumeResultsReady() throws MessagingException {
        messagingService.createReceiver(TopicsConfig.HA_TOPIC, resultsReadyConsumer);
    }
    
    @Override
    public void shutdown() throws MessagingException {
        LOG.info("OR - Disconnecting from the partial results ready notification topic");
        stopConsumingResultsReady();
        LOG.info("OR - Disconnecting from the MixDec key generation queue");
        stopKeyGenerationConsumingContributions();
    }

    /**
     * @throws MessagingException
     */
    private void stopKeyGenerationConsumingContributions()
            throws MessagingException {
        Queue[] queues = QueuesConfig.MIX_DEC_KEY_GENERATION_RES_QUEUES;
        
        if (queues == null || queues.length == 0) {
            throw new IllegalStateException("No MixDec key generation response queues provided to listen to.");
        }
        
        for (Queue queue : queues) {
            messagingService.destroyReceiver(queue, contributionsConsumer);
        }
    }
    
    private void stopConsumingResultsReady() throws MessagingException {
        messagingService.destroyReceiver(TopicsConfig.HA_TOPIC, resultsReadyConsumer);
    }

    private void publishToReqQ(KeyCreationDTO keyCreationDTO) throws IOException, ResourceNotFoundException {

        Queue[] queues = QueuesConfig.MIX_DEC_KEY_GENERATION_REQ_QUEUES;

        if (queues == null || queues.length == 0) {
            throw new IllegalStateException("No MixDec key generation request queues provided to publish to.");
        }

        for (Queue queue : queues) {

            try {
                messagingService.send(queue, keyCreationDTO);
            } catch (MessagingException e) {
                LOG.error("OR - Error publishing the MixDec key generation request to the nodes queues: " + e);
                throw new ResourceNotFoundException(
                    "Error publishing the MixDec key generation request to the nodes queues. ", e);
            }
        }
    }

    private List<KeyCreationDTO> collectKeyCreationDTOMessages(KeyCreationDTO keyCreationDTO) throws ResourceNotFoundException {

        UUID correlationId = keyCreationDTO.getCorrelationId();

        List<byte[]> collectedKeys;

        try {

            LOG.info("OR - Waiting for the MixDec key generation from the nodes to be returned.");
            collectedKeys = pollingService.getResults(correlationId);

        } catch (TimeoutException e) {
            LOG.error("OR - Error collecting the MixDec key generation from the nodes: " + e);
            throw new ResourceNotFoundException("Error collecting the MixDec key generation from the nodes. ", e);
        }

        return collectedKeys.stream().map(MixDecKeyGenerationServiceImpl::deserializeKeyCreationDTO)
                .collect(Collectors.toList());
    }
    
    private static KeyCreationDTO deserializeKeyCreationDTO(byte[] bytes) {
        try {
            return (KeyCreationDTO) new StreamSerializableObjectReaderImpl<>().read(bytes);
        } catch (SafeStreamDeserializationException e) {
            throw new IllegalStateException("Failed to deserialize the key creation DTO.", e);
        }
    }

    private List<String> collectPublicKeys(List<KeyCreationDTO> collectedKeys) throws GeneralCryptoLibException {
        
        List<String> jsons = new ArrayList<>(collectedKeys.size());

        for (KeyCreationDTO dto : collectedKeys) {
            CCPublicKey key = dto.getPublicKeys().get(INDEX_OF_KEY_WITHIN_GENERATED_LIST);
            /*For each key the following JSON is created
             * {
             *    publicKey: "<ElGamalPublicKey as JSON>",
             *    signature: "<Base64 encoded signature>"
             *    signerCertificate: "<Signer certificate in PEM format>",
             *    nodeCACertificate: "<CCN CA certificate in PEM format>"
             * }
             */ 
            JsonObjectBuilder json = Json.createObjectBuilder();
            json.add("publicKey", key.getPublicKey().toJson());
            json.add("signature", Base64.getEncoder().encodeToString(key.getKeySignature()));
            json.add("signerCertificate", PemUtils.certificateToPem(key.getSignerCertificate()));
            json.add("nodeCACertificate", PemUtils.certificateToPem(key.getNodeCACertificate()));
            jsons.add(json.build().toString());
        }

        return jsons;
    }
}
