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

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore.PasswordProtection;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertificateException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assert;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.products.ov.config.it.utils.CertificateValidator;
import com.scytl.products.ov.config.it.utils.KeyLoader;
import com.scytl.products.ov.config.it.utils.KeyPairValidator;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.ConfigurationInput;
import com.scytl.products.ov.datapacks.beans.CredentialProperties;
import com.scytl.products.ov.datapacks.helpers.ReplacementsHolder;
import com.scytl.products.ov.readers.ConfigurationInputReader;
import com.scytl.products.ov.utils.ConfigFileUtils;
import com.scytl.products.ov.utils.ConfigObjectMapper;

import com.scytl.products.ov.commons.beans.AuthenticationContextData;
import com.scytl.products.ov.commons.beans.AuthenticationVoterData;
import com.scytl.products.ov.commons.beans.ElectionInformationContents;
import com.scytl.products.ov.commons.beans.VotingWorkflowContextData;

/**
 * A class to execute all validations related with Create Election Event command
 */
public class CreateElectionEventVerifier {

    private static final String OUTPUT_FOLDER = "target/it/output";

    private static final File _outputFolderFile = new File(OUTPUT_FOLDER);

    private final ConfigFileUtils _configFileUtils = new ConfigFileUtils();

    private CertificateValidator _certificateValidator;

    private final KeyPairValidator _keyPairValidator = new KeyPairValidator();

    private final ConfigurationInputReader _configInputReader = new ConfigurationInputReader();

    private final ConfigObjectMapper _mapper = new ConfigObjectMapper();

    private final ScytlKeyStoreService _scytlKeyStoreService;

    private Path _offlineFolder;

    private Path _authenticationContextDataFile;

    private Path _authenticationVoterDataFile;

    private Path _electionInformationContentsFile;

    private Path _votingWorkflowContextDataFile;

    private ConfigurationInput _configurationInput;

    private final Map<String, String> _passwordsMap = new HashMap<>();

    private final Map<String, CryptoX509Certificate> _loadedCertificates = new HashMap<>();

    /**
     * Default Constructor
     * 
     * @throws GeneralCryptoLibException
     */
    public CreateElectionEventVerifier() throws GeneralCryptoLibException {
        Security.addProvider(new BouncyCastleProvider());
        _scytlKeyStoreService = new ScytlKeyStoreService();
    }

    /**
     * @throws ParseException
     * @throws IOException
     * @throws GeneralCryptoLibException
     * @throws CertificateException
     */
    public Map<String, CryptoX509Certificate> createElectionEventValidations(final Path configProperties,
            final Path configurationJSON, final String eeid)
            throws ParseException, IOException, CertificateException, GeneralCryptoLibException {
        _certificateValidator = new CertificateValidator(new ReplacementsHolder(eeid));
        _configurationInput = _configInputReader.fromFileToJava(configurationJSON.toFile());

        validateFoldersAndFilesExistence();

        validateOfflineFiles(eeid);

        validateOnlineFiles(configProperties, eeid);

        validateChain();

        return _loadedCertificates;

    }

    private void validateFoldersAndFilesExistence() throws ParseException {

        File timestampFolder = getLastTimestamp();

        Path eeidFolder = Paths.get(timestampFolder.getAbsolutePath(), "1234");
        Assert.assertTrue(Files.exists(eeidFolder));

        _offlineFolder = Paths.get(eeidFolder.toString(), Constants.OFFLINE_DIRECTORY);
        Assert.assertTrue(Files.exists(_offlineFolder));

        Path onlineFolder = Paths.get(eeidFolder.toString(), Constants.ONLINE_DIRECTORY);
        Assert.assertTrue(Files.exists(onlineFolder));

        Path onlineAuthenticationFolder = Paths.get(onlineFolder.toString(), Constants.AUTHENTICATION_DIRECTORY);
        Assert.assertTrue(Files.exists(onlineAuthenticationFolder));

        _authenticationContextDataFile =
            Paths.get(onlineAuthenticationFolder.toString(), Constants.AUTHENTICATION_CONTEXTDATA_FILENAME);
        Assert.assertTrue(Files.exists(_authenticationContextDataFile));

        _authenticationVoterDataFile =
            Paths.get(onlineAuthenticationFolder.toString(), Constants.AUTHENTICATION_VOTERDATA_FILENAME);
        Assert.assertTrue(Files.exists(_authenticationVoterDataFile));

        Path electionInformationFolder = Paths.get(onlineFolder.toString(), Constants.ELECTION_INFORMATION_DIRECTORY);
        Assert.assertTrue(Files.exists(electionInformationFolder));

        _electionInformationContentsFile =
            Paths.get(electionInformationFolder.toString(), Constants.ELECTION_INFORMATION_CONTENTS_FILENAME);

        Assert.assertTrue(Files.exists(_electionInformationContentsFile));

        Path votingWorkflowFolder = Paths.get(onlineFolder.toString(), Constants.VOTING_WORKFLOW_DIRECTORY);
        Assert.assertTrue(Files.exists(votingWorkflowFolder));

        _votingWorkflowContextDataFile =
            Paths.get(votingWorkflowFolder.toString(), Constants.VOTING_WORKFLOW_CONTEXT_DATA_FILENAME);
        Assert.assertTrue(Files.exists(_votingWorkflowContextDataFile));
    }

    private void validateOfflineFiles(final String eeid)
            throws IOException, CertificateException, GeneralCryptoLibException {

        validateCerts(eeid);

        validatePasswordsFile();

        validateKeystores();
    }

    private void validateCerts(final String eeid) throws IOException, CertificateException, GeneralCryptoLibException {

        final Map<String, CredentialProperties> configProperties = _configurationInput.getConfigProperties();
        for (final String certName : configProperties.keySet()) {

            CredentialProperties credentialProperties = configProperties.get(certName);

            Path certPath = Paths.get(_offlineFolder.toString(), certName + Constants.PEM);

            CryptoX509Certificate certificate =
                _certificateValidator.validateCertFromPath(certPath, credentialProperties);
            _loadedCertificates.put(certName, certificate);
        }
    }

    private void validatePasswordsFile() throws IOException {

        Path passwordsFilePath = Paths.get(_offlineFolder.toString(), Constants.PW_TXT);
        for (String line : Files.readAllLines(passwordsFilePath)) {
            String[] values = line.split(",");
            _passwordsMap.put(values[0], values[1]);
        }

        final Map<String, CredentialProperties> configProperties = _configurationInput.getConfigProperties();
        for (final String certName : configProperties.keySet()) {
            Assert.assertNotNull(_passwordsMap.get(certName));
        }
    }

    private void validateKeystores() throws IOException, GeneralCryptoLibException {

        final Map<String, CredentialProperties> configProperties = _configurationInput.getConfigProperties();
        for (final String certName : configProperties.keySet()) {

            CredentialProperties credentialProperties = configProperties.get(certName);

            Path keystorePath = Paths.get(_offlineFolder.toString(), certName + Constants.SKS);
            try (InputStream in = Files.newInputStream(keystorePath)) {
                char[] password = _passwordsMap.get(certName).toCharArray();
                CryptoAPIScytlKeyStore keystore =
                    _scytlKeyStoreService.loadKeyStore(in, new PasswordProtection(password));
                PrivateKey privateKey =
                    keystore.getPrivateKeyEntry(credentialProperties.getAlias().get("privateKey"), password);
                CryptoX509Certificate cert = _loadedCertificates.get(certName);
                _keyPairValidator.validateKeyPair(cert.getPublicKey(), privateKey);
            }
        }
    }

    private void validateOnlineFiles(final Path configProperties, final String eeid)
            throws IOException, CertificateException, GeneralCryptoLibException {

        Properties props = new Properties();
        try (BufferedReader bufferedReader = Files.newBufferedReader(configProperties)) {
            props.load(bufferedReader);
        }

        validateAuthenticationVoterDataJSON(eeid);

        validateAuthenticationContextDataJSON(props, eeid);

        validateElectionInformationContentsJSON(props);

        validateVotingWorkflowContextDataJSON(props);

    }

    private void validateAuthenticationVoterDataJSON(final String eeid)
            throws IOException, CertificateException, GeneralCryptoLibException {

        AuthenticationVoterData authenticationVoterData =
            _mapper.fromJSONFileToJava(_authenticationVoterDataFile.toFile(), AuthenticationVoterData.class);

        Assert.assertEquals(eeid, authenticationVoterData.getElectionEventId());

        CryptoX509Certificate electionRootCaCryptoCert = _loadedCertificates.get("electioneventca");
        Assert.assertEquals(serializeCertificate(electionRootCaCryptoCert),
            authenticationVoterData.getElectionRootCA());

        CryptoX509Certificate authoritiescaCryptoCert = _loadedCertificates.get("authoritiesca");
        Assert.assertEquals(serializeCertificate(authoritiescaCryptoCert), authenticationVoterData.getAuthoritiesCA());

        CryptoX509Certificate servicescaCryptoCert = _loadedCertificates.get("servicesca");
        Assert.assertEquals(serializeCertificate(servicescaCryptoCert), authenticationVoterData.getServicesCA());

        CryptoX509Certificate credentialscaCryptoCert = _loadedCertificates.get("credentialsca");
        Assert.assertEquals(serializeCertificate(credentialscaCryptoCert), authenticationVoterData.getCredentialsCA());

        CryptoX509Certificate cryptoX509Certificate = KeyLoader.convertPEMStringtoCryptoX509Certificate(
                authenticationVoterData.getAuthenticationTokenSignerCert());

        _loadedCertificates.put(_configurationInput.getAuthTokenSigner().getName(), cryptoX509Certificate);

    }

    private void validateAuthenticationContextDataJSON(final Properties props, final String eeid) throws IOException {

        AuthenticationContextData authenticationContextData =
            _mapper.fromJSONFileToJava(_authenticationContextDataFile.toFile(), AuthenticationContextData.class);

        Assert.assertEquals(eeid, authenticationContextData.getElectionEventId());

        String challengeResExpTime = (String) props.get("challengeResExpTime");
        Assert.assertTrue(StringUtils.isNotEmpty(challengeResExpTime));
        String authTokenExpTime = (String) props.get("authTokenExpTime");
        Assert.assertTrue(StringUtils.isNotEmpty(authTokenExpTime));
        String challengeLength = (String) props.get("challengeLength");
        Assert.assertTrue(StringUtils.isNotEmpty(challengeLength));

        Assert.assertEquals(challengeResExpTime,
            authenticationContextData.getAuthenticationParams().getChallengeResExpTime());
        Assert.assertEquals(authTokenExpTime,
            authenticationContextData.getAuthenticationParams().getAuthTokenExpTime());
        Assert.assertEquals(challengeLength, authenticationContextData.getAuthenticationParams().getChallengeLength());

        authenticationContextData.getAuthenticationTokenSignerKeystore();
        authenticationContextData.getAuthenticationTokenSignerPassword();

    }

    private void validateElectionInformationContentsJSON(final Properties props) throws IOException {

        String numVotesPerVotingCard = (String) props.get("numVotesPerVotingCard");
        Assert.assertTrue(StringUtils.isNotEmpty(numVotesPerVotingCard));
        String numVotesPerAuthToken = (String) props.get("numVotesPerAuthToken");
        Assert.assertTrue(StringUtils.isNotEmpty(numVotesPerAuthToken));

        ElectionInformationContents electionInformationContents =
            _mapper.fromJSONFileToJava(_electionInformationContentsFile.toFile(), ElectionInformationContents.class);

        Assert.assertEquals(numVotesPerVotingCard,
            electionInformationContents.getElectionInformationParams().getNumVotesPerVotingCard());

        Assert.assertEquals(numVotesPerAuthToken,
            electionInformationContents.getElectionInformationParams().getNumVotesPerAuthToken());

        CryptoX509Certificate electionRootCaCryptoCert = _loadedCertificates.get("electioneventca");
        Assert.assertEquals(serializeCertificate(electionRootCaCryptoCert),
            electionInformationContents.getElectionRootCA());

        CryptoX509Certificate authoritiescaCryptoCert = _loadedCertificates.get("authoritiesca");
        Assert.assertEquals(serializeCertificate(authoritiescaCryptoCert),
            electionInformationContents.getAuthoritiesCA());

        CryptoX509Certificate servicescaCryptoCert = _loadedCertificates.get("servicesca");
        Assert.assertEquals(serializeCertificate(servicescaCryptoCert), electionInformationContents.getServicesCA());

        CryptoX509Certificate credentialscaCryptoCert = _loadedCertificates.get("credentialsca");
        Assert.assertEquals(serializeCertificate(credentialscaCryptoCert),
                electionInformationContents.getCredentialsCA());
    }

    private void validateVotingWorkflowContextDataJSON(final Properties props) throws IOException {
        String maxNumberOfAttempts = (String) props.get("maxNumberOfAttempts");
        Assert.assertTrue(StringUtils.isNotEmpty(maxNumberOfAttempts));

        VotingWorkflowContextData votingWorkflowContextData =
            _mapper.fromJSONFileToJava(_votingWorkflowContextDataFile.toFile(), VotingWorkflowContextData.class);

        Assert.assertEquals(maxNumberOfAttempts, votingWorkflowContextData.getMaxNumberOfAttempts());

    }

    private void validateChain() throws GeneralCryptoLibException {

        // 1) chain: electionEventCA -> authoritiesCA -> adminBoard

        Assert.assertTrue(_certificateValidator.checkChain(_loadedCertificates.get("electioneventca"),
            _loadedCertificates.get("authoritiesca"), _loadedCertificates.get("adminboard")).size() == 0);

        // 2) chain: electionEventCA -> servicesCA -> authTokenSigner

        Assert.assertTrue(_certificateValidator.checkChain(_loadedCertificates.get("electioneventca"),
            _loadedCertificates.get("servicesca"), _loadedCertificates.get("authTokenSigner")).size() == 0);

    }

    private File getLastTimestamp() throws ParseException {
        return _configFileUtils.getLastFolderTimestamp(_outputFolderFile);
    }

    private String serializeCertificate(final CryptoAPIX509Certificate certificate) {

        final byte[] certPEMArray = certificate.getPemEncoded();
        return new String(certPEMArray, StandardCharsets.UTF_8);
    }

}
