/*
 * 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.config.commands.ballotbox;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PrivateKey;

import org.apache.commons.configuration.ConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.LoggingWriter;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.commons.beans.BallotBox;
import com.scytl.products.ov.commons.beans.BallotBoxContextData;
import com.scytl.products.ov.commons.beans.ElectionEvent;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.config.commands.api.output.BallotBoxesServiceOutput;
import com.scytl.products.ov.config.commands.electionevent.datapacks.beans.ElectionCredentialDataPack;
import com.scytl.products.ov.config.commands.electionevent.datapacks.beans.ElectionInputDataPack;
import com.scytl.products.ov.config.commands.electionevent.datapacks.generators.ElectionCredentialDataPackGenerator;
import com.scytl.products.ov.config.exceptions.CreateBallotBoxesException;
import com.scytl.products.ov.config.exceptions.specific.GenerateBallotBoxesException;
import com.scytl.products.ov.config.logevents.ConfigGeneratorLogEvents;
import com.scytl.products.ov.config.logs.Loggable;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.CredentialProperties;
import com.scytl.products.ov.datapacks.helpers.ReplacementsHolder;
import com.scytl.products.ov.utils.ConfigObjectMapper;
import com.scytl.products.ov.utils.KeyStoreReader;

/**
 * Generates a given number of {@link BallotBox}, which are linked to the provided {@link Ballot}.
 */
public class BallotBoxGenerator {

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

    private final ElectionCredentialDataPackGenerator electionCredentialDataPackGenerator;

    private final ConfigObjectMapper mapper;

    @Loggable
    private LoggingWriter logWriter;

    public BallotBoxGenerator(final ElectionCredentialDataPackGenerator electionCredentialDataPackGenerator) {
        this.electionCredentialDataPackGenerator = electionCredentialDataPackGenerator;
        this.mapper = new ConfigObjectMapper();
    }

    /**
     * Generates as many {@link BallotBox} as ballot box IDs passed as parameter.
     *
     * @param ballotBoxHolder
     * @return
     * @throws URISyntaxException
     */
    public BallotBoxesServiceOutput generate(final BallotBoxParametersHolder ballotBoxHolder) {

        final String ballotBoxID = ballotBoxHolder.getBallotBoxID();

        final String alias = ballotBoxHolder.getAlias();

        final String ballotID = ballotBoxHolder.getBallotID();

        final String electoralAutorityId = ballotBoxHolder.getElectoralAuthorityID();

        final String isTest = ballotBoxHolder.getTest();

        final String gracePeriod = ballotBoxHolder.getGracePeriod();

        final String confirmationRequired = ballotBoxHolder.getConfirmationRequired();

        final Path absoluteOutputPath = ballotBoxHolder.getOutputPath();

        if (!doesBallotIdMatchAnyExistingBallot(ballotID,
            Paths
                .get(absoluteOutputPath.toString(), Constants.ONLINE_DIRECTORY,
                    Constants.ELECTION_INFORMATION_DIRECTORY, Constants.BALLOTS_DIRECTORY, ballotID)
                .toAbsolutePath())) {
            throw new GenerateBallotBoxesException("The specified Ballot ID does not match any existing ballot.");
        }

        final String eeID = ballotBoxHolder.getEeID();

        final Path absoluteOnlinePath = Paths
            .get(absoluteOutputPath.toString(), Constants.ONLINE_DIRECTORY, Constants.ELECTION_INFORMATION_DIRECTORY,
                Constants.BALLOTS_DIRECTORY, ballotID, Constants.BALLOT_BOXES_DIRECTORY)
            .toAbsolutePath();

        final EncryptionParameters encParams = ballotBoxHolder.getEncryptionParameters();

        final String writeInAlphabet = ballotBoxHolder.getWriteInAlphabet();

        final CryptoAPIX509Certificate servicesCACert = ballotBoxHolder.getServicesCACert();

        final CryptoAPIX509Certificate electionCACert = ballotBoxHolder.getElectionCACert();

        final CredentialProperties ballotBoxCredentialProperties = ballotBoxHolder.getBallotBoxCredentialProperties();

        final ElectionInputDataPack electionInputDataPack = ballotBoxHolder.getInputDataPack();

        electionInputDataPack.setCredentialProperties(ballotBoxCredentialProperties);

        final PrivateKey servicesCAPrivKey = ballotBoxHolder.getSignerPrivateKey();

        electionInputDataPack.setParentKeyPair(new KeyPair(servicesCACert.getPublicKey(), servicesCAPrivKey));

        String keyForProtectingKeystorePassword = ballotBoxHolder.getKeyForProtectingKeystorePassword();

        LOG.info("Generating ballot box " + ballotBoxID + "...");
        final Path onlinePath = Paths.get(absoluteOnlinePath.toString(), ballotBoxID);

        createDirectories(onlinePath);

        // create new replacements
        final ReplacementsHolder replacementsHolder = new ReplacementsHolder(eeID, ballotBoxID);

        final ElectionCredentialDataPack dataPack;

        try {
            dataPack = electionCredentialDataPackGenerator.generate(electionInputDataPack, replacementsHolder,
                Constants.CONFIGURATION_BALLOTBOX_JSON_TAG, keyForProtectingKeystorePassword,
                ballotBoxHolder.getCertificateProperties(), servicesCACert, electionCACert);
        } catch (final IOException | GeneralCryptoLibException | ConfigurationException e) {
            throw new CreateBallotBoxesException(
                "An error occurred while generating the ballot box data pack: " + e.getMessage(), e);
        }

        try {
            storeOnlineBallotBox(ballotID, eeID, ballotBoxID, alias, electoralAutorityId, isTest, gracePeriod,
                confirmationRequired, encParams, writeInAlphabet, dataPack, onlinePath);
        } catch (final IOException e) {
            throw new CreateBallotBoxesException(
                "An error occurred while saving info on online ballot box id folder: " + e.getMessage(), e);
        } finally {
            dataPack.clearPassword();
        }

        logWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ConfigGeneratorLogEvents.GENBB_SUCCESS_CREATED_AND_STORED)
                .electionEvent(ballotBoxHolder.getEeID()).user("adminID").additionalInfo("bbid", ballotBoxID)
                .createLogInfo());

        LOG.info("Ballot box " + ballotBoxID + " was successfully created");

        final Path[] outputPaths = new Path[2];
        outputPaths[0] = absoluteOnlinePath;

        BallotBoxesServiceOutput ballotBoxesServiceOutput = new BallotBoxesServiceOutput();
        ballotBoxesServiceOutput.setOutputPath(outputPaths[0].toString());
        return ballotBoxesServiceOutput;
    }

    private boolean doesBallotIdMatchAnyExistingBallot(final String ballotID, final Path ballotsDirectoryPath) {

        final Ballot ballotFromFile;
        final Path enrichedBallotPath = Paths.get(ballotsDirectoryPath.toString(), Constants.ENRICHED_BALLOT_FILENAME);

        try {
            ballotFromFile = mapper.fromJSONFileToJava(enrichedBallotPath.toFile(), Ballot.class);

            if (ballotFromFile.getId().equals(ballotID)) {
                return true;
            }
        } catch (final IOException e) {
            throw new CreateBallotBoxesException("An error reconstructing a ballot from a JSON file: " + e.getMessage(),
                e);
        }

        return false;
    }

    private void storeOnlineBallotBox(final String ballotID, final String eeID, final String bbid, final String alias,
            final String electoralAuthorityId, final String isTest, final String gracePeriod,
            String confirmationRequired, final EncryptionParameters encParams, final String writeInAlphabet,
            final ElectionCredentialDataPack dataPack, final Path onlinePath) throws IOException {

        final BallotBox ballotBox = new BallotBox();

        ballotBox.setBid(ballotID);
        ballotBox.setEeid(eeID);
        ballotBox.setId(bbid);
        ballotBox.setAlias(alias);
        ballotBox.setBallotBoxCert(new String(dataPack.getCertificate().getPemEncoded(), StandardCharsets.UTF_8));
        ballotBox.setStartDate(dataPack.getStartDate().toString());
        ballotBox.setEndDate(dataPack.getEndDate().toString());
        ballotBox.setConfirmationRequired(Boolean.parseBoolean(confirmationRequired));

        ballotBox.setElectoralAuthorityId(electoralAuthorityId);
        ballotBox.setTest(Boolean.parseBoolean(isTest));
        ballotBox.setGracePeriod(gracePeriod);
        final EncryptionParameters encryptionParameters =
            new EncryptionParameters(encParams.getP(), encParams.getQ(), encParams.getG());

        ballotBox.setEncryptionParameters(encryptionParameters);
        ballotBox.setWriteInAlphabet(writeInAlphabet);

        final Path pathFile = Paths.get(onlinePath.toString(), Constants.BALLOT_BOX_FILENAME);
        mapper.fromJavaToJSONFileWithoutNull(ballotBox, pathFile.toFile());

        BallotBoxContextData ballotBoxContextData = new BallotBoxContextData();
        ballotBoxContextData.setElectionEvent(new ElectionEvent(eeID));
        ballotBoxContextData.setId(bbid);
        ballotBoxContextData.setKeystore(KeyStoreReader.toString(dataPack.getKeyStore(), dataPack.getPassword()));
        ballotBoxContextData.setPasswordKeystore(dataPack.getEncryptedPassword());

        final Path ballotBoxContextDataPath =
            Paths.get(onlinePath.toString(), Constants.BALLOTBOX_CONTEXTDATA_FILENAME);
        mapper.fromJavaToJSONFileWithoutNull(ballotBoxContextData, ballotBoxContextDataPath.toFile());

    }

    private void createDirectories(final Path onlinePath) {
        try {
            Files.createDirectories(onlinePath);
        } catch (final IOException e) {
            throw new IllegalArgumentException(
                "An error occurred while creating the following path: " + onlinePath.toString(), e);
        }
    }

}
