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

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jcsv.writer.CSVWriter;
import com.googlecode.jcsv.writer.internal.CSVWriterBuilder;
import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.certificates.CertificatesServiceAPI;
import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.certificates.bean.CertificateData;
import com.scytl.cryptolib.certificates.bean.ValidityDates;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.service.CertificatesService;
import com.scytl.cryptolib.elgamal.bean.CiphertextImpl;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.mathematical.groups.activity.GroupElementsCompressor;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedMixerConfig;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedVerifierConfig;
import com.scytl.products.ov.mixnet.commons.io.ElGamalEncryptedBallotEntryConverter;

public class TestDataProvider {

    public static final int NUMBER_OF_VOTES = 10;

	private ObjectMapper mapper = new ObjectMapper();

    private AsymmetricServiceAPI asymmetricService;

    private CertificatesServiceAPI certificatesService;

    private Path elGamalEncryptedBallotsPath;

    private ElGamalServiceAPI elGamalService;

    private List<List<BigInteger>> inputList = new ArrayList<>();

    private ZpSubgroup group_g2_q11;

    private ElGamalKeyPair elGamalKeyPair;

    public String createTestDataConfig() throws GeneralCryptoLibException, KeyException, IOException {
        Path targetPath = Paths.get("target");
        Path outputDirectory = targetPath.resolve("outputs");
        Path inputDirectory = targetPath.resolve("inputs");
        elGamalEncryptedBallotsPath = inputDirectory;
        inputDirectory.toFile().mkdirs();
        outputDirectory.toFile().mkdirs();
        Path appconfigLocation = inputDirectory.resolve("applicationConfig.json");

        Security.addProvider(new BouncyCastleProvider());
        BigInteger p = new BigInteger("16370518994319586760319791526293535327576438646782139419846004180837103527129035954742043590609421369665944746587885814920851694546456891767644945459124422553763416586515339978014154452159687109161090635367600349264934924141746082060353483306855352192358732451955232000593777554431798981574529854314651092086488426390776811367125009551346089319315111509277347117467107914073639456805159094562593954195960531136052208019343392906816001017488051366518122404819967204601427304267380238263913892658950281593755894747339126531018026798982785331079065126375455293409065540731646939808640273393855256230820509217411510058759");
        BigInteger q = new BigInteger("8185259497159793380159895763146767663788219323391069709923002090418551763564517977371021795304710684832972373293942907460425847273228445883822472729562211276881708293257669989007077226079843554580545317683800174632467462070873041030176741653427676096179366225977616000296888777215899490787264927157325546043244213195388405683562504775673044659657555754638673558733553957036819728402579547281296977097980265568026104009671696453408000508744025683259061202409983602300713652133690119131956946329475140796877947373669563265509013399491392665539532563187727646704532770365823469904320136696927628115410254608705755029379");
        BigInteger g = new BigInteger("2");
        group_g2_q11 = new ZpSubgroup(g, p, q);
        asymmetricService = new AsymmetricService();
        certificatesService = new CertificatesService();
        elGamalService = new ElGamalService();

        elGamalKeyPair = createElGamalKeyPair();
        
        ElGamalPublicKey elGamalPublicKey = elGamalKeyPair.getPublicKeys().multiply(elGamalKeyPair.getPublicKeys());

        Path publicKeyLocation = createPublicKey(inputDirectory.resolve("publicKey.json"), elGamalPublicKey);
        Path privateKeyLocation = createPrivateKey(inputDirectory.resolve("ballotDecryptionKey.json"), elGamalKeyPair.getPrivateKeys());
        writeEncryptionParameters(inputDirectory.resolve("encryptionParameters.json"));

        Path createBallotBoxToBeMixed = createBallotBoxToBeMixed();

        FileBasedApplicationConfig generatedConfig = new FileBasedApplicationConfig();
        generatedConfig.setConcurrencyLevel(Integer.valueOf(1));
        generatedConfig.setNumberOfParallelBatches(Integer.valueOf(1));
        generatedConfig.setNumOptions(Integer.valueOf(1));
        generatedConfig.setMinimumBatchSize(64);
        FileBasedMixerConfig mixer = new FileBasedMixerConfig();
        mixer.setMix(true);
        Map<String, String> inputMap = new HashMap<>();

        inputMap.put("ballotDecryptionKey", privateKeyLocation.toFile().getAbsolutePath().replaceAll(".json", ""));
        inputMap.put("publicKey", publicKeyLocation.toFile().getAbsolutePath().replaceAll(".json", ""));
        inputMap.put("encryptedBallots", createBallotBoxToBeMixed.toFile().getAbsolutePath().replaceAll(".csv", ""));
        inputMap.put("encryptionParameters",
            inputDirectory.resolve("encryptionParameters").toFile().getAbsolutePath().replaceAll(".json", ""));

        mixer.setInput(inputMap);
        String outputAbsPath = outputDirectory.toFile().getAbsolutePath();
        mixer.setOutputDirectory(outputAbsPath);
        generatedConfig.setMixer(mixer);
        FileBasedVerifierConfig verifier = new FileBasedVerifierConfig();
        verifier.setVerify(true);
        verifier.setInputDirectory(outputAbsPath);
        verifier.setOutputDirectory(outputAbsPath);
        generatedConfig.setVerifier(verifier);
        mapper.writeValue(appconfigLocation.toFile(), generatedConfig);

        return appconfigLocation.toFile().getAbsolutePath();
    }

    private void writeEncryptionParameters(Path resolve)
            throws IOException, GeneralCryptoLibException {
        final List<String> linesToBeWritten = new ArrayList<>();
        linesToBeWritten.add(group_g2_q11.toJson());
        FileUtils.writeLines(resolve.toFile(), linesToBeWritten);

    }

    private ElGamalKeyPair createElGamalKeyPair() throws KeyException, GeneralCryptoLibException {
		ElGamalPublicKey publicKey = ElGamalPublicKey.fromJson(KeyManagerMockConfig.PUBLIC_KEY);
		ElGamalPrivateKey privateKey = ElGamalPrivateKey.fromJson(KeyManagerMockConfig.PRIVATE_KEY);
        return  new ElGamalKeyPair(privateKey, publicKey);

    }

    private Path createBallotBoxToBeMixed()
            throws GeneralCryptoLibException, JsonParseException, JsonMappingException, IOException {

        Ballot ballot = mapper.readValue(getClass().getClassLoader().getResourceAsStream("input/ballot.json"), Ballot.class);

        List<String> representations = new ArrayList<>();
        ballot.getContests().forEach(
            contest -> contest.getOptions().forEach(option -> representations.add(option.getRepresentation())));

        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(elGamalKeyPair.getPublicKeys());
        List<ElGamalComputationsValues> ciphertexts = new ArrayList<>();

        GroupElementsCompressor<ZpGroupElement> compressor = new GroupElementsCompressor<>();

        for (int i = 0; i < NUMBER_OF_VOTES; i++) {
            Collections.shuffle(representations);
            List<ZpGroupElement> zpGroupElements =
                prepareMessage(representations.get(0), representations.get(1), representations.get(2));

            insertToInputList(representations.get(0), representations.get(1), representations.get(2));

            ZpGroupElement compressedElement = compressor.compress(zpGroupElements);
            List<ZpGroupElement> compressedMessage = new ArrayList<>();
            compressedMessage.add(compressedElement);

            ElGamalComputationsValues ciphertext =
                encrypter.encryptGroupElements(compressedMessage).getComputationValues();
            ciphertexts.add(ciphertext);
        }

        List<Ciphertext> elGamalEncryptedBallots =
            fromElGamalComputationsValuesToElGamalEncryptedBallot(ciphertexts);

        Path ballots = elGamalEncryptedBallotsPath.resolve("encryptedBallots.csv");
        try (final Writer out = new FileWriter(ballots.toFile());
                final CSVWriter<Ciphertext> csvWriter = new CSVWriterBuilder<Ciphertext>(out)
                    .entryConverter(new ElGamalEncryptedBallotEntryConverter()).build()) {
            csvWriter.writeAll(elGamalEncryptedBallots);
            csvWriter.flush();
        }

        generateTestCertAndSign(elGamalEncryptedBallotsPath);
        return ballots;
    }

    private void insertToInputList(final String... values) {

        List<BigInteger> list = new ArrayList<>();
        for (String value : values) {
            list.add(new BigInteger(value));
        }
        inputList.add(list);
    }

    private X509Certificate generateTestCertAndSign(final Path... paths) throws GeneralCryptoLibException, IOException {
        CryptoAPIX509Certificate cert = createTestCertificate();

        for (Path path : paths) {
            Path signaturePath = Paths.get(path + ".metadata");
            String signatureB64 = new String();
            Files.deleteIfExists(signaturePath);
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(signaturePath.toFile()))) {
                writer.write(signatureB64);
            }
        }
        return cert.getCertificate();
    }

	public CryptoAPIX509Certificate createTestCertificate() throws GeneralCryptoLibException {
		KeyPair keyPair = asymmetricService.getKeyPairForSigning();

        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
        ZonedDateTime end = now.plusYears(1);
        ValidityDates validityDates = new ValidityDates(Date.from(now.toInstant()), Date.from(end.toInstant()));

        CertificateData certificateData = new CertificateData();
        certificateData.setSubjectPublicKey(keyPair.getPublic());
        certificateData.setValidityDates(validityDates);
        X509DistinguishedName distinguishedName = new X509DistinguishedName.Builder("certId", "ES").build();
        certificateData.setSubjectDn(distinguishedName);
        certificateData.setIssuerDn(distinguishedName);

        return certificatesService.createSignX509Certificate(certificateData, keyPair.getPrivate());
	}

    private List<ZpGroupElement> prepareMessage(final String... numbers) throws GeneralCryptoLibException {

        List<ZpGroupElement> messages = new ArrayList<>();

        for (String number : numbers) {
            ZpGroupElement element = new ZpGroupElement(new BigInteger(number), group_g2_q11);
            messages.add(element);
        }

        return messages;
    }

    private List<Ciphertext> fromElGamalComputationsValuesToElGamalEncryptedBallot(
            final List<ElGamalComputationsValues> ciphertexts) throws GeneralCryptoLibException {

        List<Ciphertext> encryptedBallots = new ArrayList<>();
        for (ElGamalComputationsValues ciphertext : ciphertexts) {
            List<ZpGroupElement> values = ciphertext.getPhis();
            List<ZpGroupElement> elements = new ArrayList<>(ciphertext.getValues().size());
            for (ZpGroupElement zpGroupElement : values) {
            	ZpGroupElement element = new ZpGroupElement(zpGroupElement.getValue(), group_g2_q11);
                elements.add(element);
            }
            Ciphertext elGamalEncryptedBallot = new CiphertextImpl(ciphertext.getGamma(), elements);
            encryptedBallots.add(elGamalEncryptedBallot);
        }

        return encryptedBallots;
    }

    private Path createPublicKey(Path inputFilePath, ElGamalPublicKey publicKeys) throws IOException, GeneralCryptoLibException {
        try (FileWriter fw = new FileWriter(inputFilePath.toFile()); PrintWriter pw = new PrintWriter(fw)) {
            pw.write(publicKeys.toJson());
        }
        return inputFilePath;
    }

    private Path createPrivateKey(Path inputFilePath, ElGamalPrivateKey privateKeys)
            throws IOException, GeneralCryptoLibException {
        try (FileWriter fw = new FileWriter(inputFilePath.toFile()); PrintWriter pw = new PrintWriter(fw)) {
			pw.write(privateKeys.toJson());
        }
        return inputFilePath;
    }
}
