/*
 * 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.mixnet.webapp.mvc;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.commons.beans.domain.model.messaging.InvalidSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadVerificationException;
import com.scytl.products.ov.commons.beans.domain.model.vote.EncryptedVote;
import com.scytl.products.ov.commons.beans.domain.model.vote.VoteSetId;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.dto.mixing.MixingPayloadImpl;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.verify.PayloadVerifier;
import com.scytl.products.ov.mixnet.commons.configuration.ApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedMixerConfig;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationConstants;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.exceptions.MixingException;
import com.scytl.products.ov.mixnet.commons.exceptions.VerifierException;
import com.scytl.products.ov.mixnet.mvc.MixingController;
import com.scytl.products.ov.mixnet.mvc.VerifyingController;
import com.scytl.products.ov.validation.ValidationController;
import com.scytl.products.ov.validation.ValidationException;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

/**
 * Main Controller for Mix/dec/val operations.
 */
@Controller
@Api(value = "Mixing, decryption and validation REST API")
public class MixDecValController {
		
    @Autowired
    private TransactionInfoProvider transactionInfoProvider;

    @Autowired
    private MixingController mixingController;

    @Autowired
    private VerifyingController verifyingController;

    @Autowired
    private ValidationController validationController;

    @Autowired
    private PayloadVerifier payloadVerifier;

    /**
     * HTTP endpoint for connectivity validation purposes
     */
    @RequestMapping(value = "/check", method = RequestMethod.GET)
    @ResponseBody
    @ApiOperation(value = "Health Check Service", notes = "Service to validate application is up & running. ")
    @ApiResponses(value = {@ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<Void> serviceCheck() {
        return new ResponseEntity<>(HttpStatus.OK);
    }

    /**
     * HTTP endpoint to execute Mixnet.
     *
     * @param input
     *            the configuration that will be passed to each of the services
     */
    @RequestMapping(value = "/execute", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    @ApiOperation(value = "Execute Mixing Service", notes = "Service to begin Mixing process. "
        + "Receives the JSON file format used until now with mixing JAR.")
    @ApiResponses(value = {@ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<MixDecValResult> execute(final HttpServletRequest httpServletRequest,
            @RequestBody final MixDecValConfig input) {
        try {
            if (input.getValidator() == null) {
                throw new IllegalArgumentException("The validator settings are empty");
            }
            if (input.getMixer() == null) {
                throw new IllegalArgumentException("The mixing settings are empty");
            }

            String tenantId = input.getValidator().getTenantId();
            MixDecValConfig localCopy = input;
            transactionInfoProvider.generate(tenantId, httpServletRequest.getRemoteAddr(),
                httpServletRequest.getLocalAddr());

            List<EncryptedVote> votes = loadVotes(input.getMixer());

            ApplicationConfig applicationConfig = MixDecValConfig.convertToStreamConfig(input, votes);

            LocationsProvider locationsProvider =
                LocationsProvider.build(applicationConfig);

            mixingController.mix(applicationConfig, locationsProvider);

            // Get a list of the folders where the output of the mixing and
            // decryption will be stored.
            List<Path> mixingOutputPaths = getMixingDirectories(Paths.get(input.getMixer().getOutputDirectory()));

            // SV-6489 refactor so that the next line is not required.
            localCopy.getMixer().setOutputDirectory(null);

            verifyingController.verify(applicationConfig, locationsProvider);

            validationController.validate(input.getValidator(), mixingOutputPaths);

            return ResponseEntity.ok(new MixDecValResult(mixingOutputPaths));
        } catch (ResourceNotFoundException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new MixDecValResult(ex));
        } catch (MixingException | VerifierException | ValidationException ex) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new MixDecValResult(ex));
        } catch (IllegalArgumentException | IOException ex) {
            return ResponseEntity.badRequest().body(new MixDecValResult(ex));
        }
    }

    private List<EncryptedVote> loadVotes(FileBasedMixerConfig mixer) throws IOException {

        // Find all files for this ballot box,
        String encryptedBallotsPathsStrings = mixer.getInput().get(LocationConstants.ENCRYPTED_BALLOTS_JSON_TAG);
        List<Path> encryptedBallotsPaths =
            Arrays.asList(ObjectMappers.fromJson(encryptedBallotsPathsStrings, Path[].class));
        X509Certificate platformCertificate;
        try {
            platformCertificate = (X509Certificate) PemUtils.certificateFromPem(mixer.getInput().get("platformRoot"));
        } catch (GeneralCryptoLibException e) {
            throw new IllegalArgumentException("Could not retrieve platform root certificate from PEM", e);
        }
        MixingPayload mixingPayload = validateAndGetFinalMixingPayload(encryptedBallotsPaths, platformCertificate);

        return mixingPayload.getVotes();
    }

    private MixingPayload validateAndGetFinalMixingPayload(List<Path> encryptedBallotsPaths,
            X509Certificate platformCertificate) {

        List<MixingPayload> orderedPayloads = getByProcessingOrder(encryptedBallotsPaths);

        for (MixingPayload mixingPayload : orderedPayloads) {
            try {
                validateSignature(mixingPayload, platformCertificate);
            } catch (PayloadVerificationException | InvalidSignatureException e) {
                throw new MixingException("An error occured validating the mixing input signature", e);
            }
        }

        return orderedPayloads.get(orderedPayloads.size() - 1);
    }

    /**
     * Ensures that a payload's signature is valid.
     *
     * @param payload
     *            the payload to validate
     * @throws InvalidSignatureException
     * @throws PayloadSignatureException
     */
    private void validateSignature(MixingPayload payload, X509Certificate platformCertificate)
            throws PayloadVerificationException, InvalidSignatureException {
        VoteSetId voteSetId = payload.getVoteSetId();

        if (payload.getSignature() == null) {
            throw new MixingException("Mixing input " + voteSetId + " does not contain a signature to validate");
        }

        if (!payloadVerifier.isValid(payload, platformCertificate)) {
            throw new InvalidSignatureException(voteSetId);
        }

    }

    private static List<MixingPayload> getByProcessingOrder(List<Path> encryptedBallotsPaths) {

        // Each output MixingPayload includes the vote encryption key resulting
        // of processing the previous input MixingPayload and the one resulting
        // of processing the output MixingPayload itself. We can use that as a
        // reference to order them.

        Map<ElGamalPublicKey, MixingPayload> mixingPayloadsByPreviousVoteEncryptionKey = new HashMap<>();
        Map<ElGamalPublicKey, MixingPayload> mixingPayloadsByVoteEncryptionKey = new HashMap<>();

        for (Path path : encryptedBallotsPaths) {
            MixingPayload mixingPayload;
            try {
                mixingPayload = ObjectMappers.fromJson(path, MixingPayloadImpl.class);
                mixingPayloadsByPreviousVoteEncryptionKey.put(mixingPayload.getPreviousVoteEncryptionKey(),
                    mixingPayload);
                mixingPayloadsByVoteEncryptionKey.put(mixingPayload.getVoteEncryptionKey(), mixingPayload);
            } catch (IOException e) {
                throw new MixingException("Could not load all the configuration paths as mixing inputs", e);
            }
        }

        // The final encryption key will never be referenced as a previous
        // encryption key
        Set<ElGamalPublicKey> previousVoteEncryptionKeys = mixingPayloadsByPreviousVoteEncryptionKey.keySet();
        Optional<ElGamalPublicKey> finalEncryptionKey = mixingPayloadsByVoteEncryptionKey.keySet().stream()
            .filter(key -> !previousVoteEncryptionKeys.contains(key)).findAny();

        if (!finalEncryptionKey.isPresent()) {
            throw new MixingException("Could not find a final mixing input");
        }

        List<MixingPayload> orderedPayloads =
            getPreviousStartingByVoteEncryptionKey(finalEncryptionKey.get(), mixingPayloadsByVoteEncryptionKey);

        if (!orderedPayloads.containsAll(mixingPayloadsByPreviousVoteEncryptionKey.values())) {
            throw new MixingException("Ordered mixing inputs do not include all the loaded inputs");
        }

        return orderedPayloads;

    }

    /**
     * @param currentVoteEncryptionKey
     *            the vote encryption key for which we want to get all previous
     *            payloads
     * @param mixingPayloadsByVoteEncryptionKey
     *            a map with all the payloads indexed by their vote encryption
     *            key
     * @return a list of payloads ordered by their vote encryption keys with the
     *         payload containing currentVoteEncryptionKey as the last element
     */
    private static List<MixingPayload> getPreviousStartingByVoteEncryptionKey(ElGamalPublicKey currentVoteEncryptionKey,
            Map<ElGamalPublicKey, MixingPayload> mixingPayloadsByVoteEncryptionKey) {

        MixingPayload currentPayload = mixingPayloadsByVoteEncryptionKey.get(currentVoteEncryptionKey);

        if (currentPayload == null) {
            return new ArrayList<>();
        } else {
            List<MixingPayload> previousPayloads = getPreviousStartingByVoteEncryptionKey(
                currentPayload.getPreviousVoteEncryptionKey(), mixingPayloadsByVoteEncryptionKey);
            previousPayloads.add(currentPayload);
            return previousPayloads;
        }

    }

    private List<Path> getMixingDirectories(Path outputPath) throws ResourceNotFoundException {

        List<Path> directories = new ArrayList<>();
        try {
            Files.walk(outputPath, 1).filter(f -> Files.isDirectory(f)).filter(f -> !f.equals(outputPath))
                .forEach(directories::add);

            directories.sort((o1, o2) -> {
                String o1s = o1.getFileName().toString();
                String o2s = o2.getFileName().toString();

                return Integer.valueOf(o1s).compareTo(Integer.valueOf(o2s));
            });
            return directories;
        } catch (IOException e) {
            throw new ResourceNotFoundException("Error accessing mixing proofs", e);
        }
    }

}
