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

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.sql.SQLException;
import java.time.ZonedDateTime;
import java.util.UUID;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;

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.service.CertificatesService;
import com.scytl.cryptolib.elgamal.cryptoapi.EncryptionParameters;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.stores.service.StoresService;

import oracle.jdbc.pool.OracleDataSource;

/**
 * Application for integration tests of the key management.
 */
@Configuration
public class Application implements ApplicationRunner, ExitCodeGenerator {
    private static final Logger LOGGER =
        LoggerFactory.getLogger(Application.class);

    @Autowired
    private ElGamalServiceAPI elGamalService;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private KeyManager keyManager;

    @Autowired
    private NodeKeysActivator nodeKeysActivator;

    private int exitCode;

    private Path keyStoreFile;

    private Path passwordFile;

    private String alias;

    private String electionEventId;

    private String electoralAuthorityId;

    private String verificationCardSetId;

    /**
     * The main method. Supports the following command line: <pre>
     * <code>
     * --keyStore=[key store file] --password=[password file] --alias=[CCN CA alias]
     * </code> </pre>
     * 
     * @param args
     */
    public static void main(String[] args) {
        SpringApplication
            .exit(SpringApplication.run(Application.class, args));
    }

    @Bean
    public ElGamalServiceAPI elGamalService()
            throws GeneralCryptoLibException {
        return new ElGamalService();
    }

    @Override
    public int getExitCode() {
        return exitCode;
    }

    @Bean
    public KeyManager keyManager(
            @Qualifier("transactionAware") DataSource dataSource,
            ElGamalServiceAPI elGamalService)
            throws GeneralCryptoLibException {
        return new KeyManagerImpl.Builder().setDataSource(dataSource)
            .setNodeId("test")
            .setAsymmetricService(new AsymmetricService())
            .setElGamalService(elGamalService)
            .setPrimitivesService(new PrimitivesService())
            .setStoresService(new StoresService())
            .setCertificatesService(new CertificatesService()).build();
    }

    @Bean
    public NodeKeysActivator nodeKeysActivator(KeyManager manager) {
        return new NodeKeysActivatorImpl(manager);
    }

    @Bean
    @Qualifier("raw")
    public DataSource rawDataSource() throws SQLException {
        OracleDataSource dataSource = new OracleDataSource();
        dataSource.setURL("jdbc:oracle:thin:@localhost:1521:xe");
        dataSource.setUser("CC");
        dataSource.setPassword("CC");
        return dataSource;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        if (!args.containsOption("keyStore")) {
            exitCode = -1;
            throw new IllegalArgumentException(
                "Key store file must be specified.");
        }
        keyStoreFile = Paths.get(args.getOptionValues("keyStore").get(0));
        if (!args.containsOption("password")) {
            exitCode = -1;
            throw new IllegalArgumentException(
                "Password file must be specified.");
        }
        passwordFile = Paths.get(args.getOptionValues("password").get(0));
        if (!args.containsOption("alias")) {
            exitCode = -1;
            throw new IllegalArgumentException("Alias must be specified.");
        }
        alias = args.getOptionValues("alias").get(0);
        electionEventId = UUID.randomUUID().toString();
        electoralAuthorityId = UUID.randomUUID().toString();
        verificationCardSetId = UUID.randomUUID().toString();

        transactionTemplate.execute(this::activateNodeKeys);
        transactionTemplate.execute(this::createElectionSigningKeys);
        transactionTemplate.execute(this::createMixingKeys);
        transactionTemplate.execute(this::createChoiceCodeKeys);
        transactionTemplate.execute(this::printNodeKeys);
        transactionTemplate.execute(this::printElectionSigningKeys);
        transactionTemplate.execute(this::printMixingKeys);
        transactionTemplate.execute(this::printChoiceCodeKeys);
        exitCode = 0;
    }

    @Bean
    @Qualifier("transactionAware")
    public DataSource transactionAwareDataSource(
            @Qualifier("raw") DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(
            @Qualifier("raw") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public TransactionTemplate transactionTemplate(
            PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }

    private Void activateNodeKeys(TransactionStatus status) {
        try {
            nodeKeysActivator.activateNodeKeys(keyStoreFile, alias,
                passwordFile);
        } catch (InvalidKeyStoreException | InvalidPasswordException
                | InvalidNodeCAException | IOException e) {
            LOGGER.warn("Failed to activate the node keys.", e);
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to activate the node keys.", e);
        }
        return null;
    }

    private Void createChoiceCodeKeys(TransactionStatus status) {
        EncryptionParameters parameters =
            elGamalService.generateEncryptionParameters();
        ChoiceCodeKeysSpec spec = new ChoiceCodeKeysSpec.Builder()
            .setElectionEventId(electionEventId)
            .setVerificationCardSetId(verificationCardSetId)
            .setParameters(parameters).setGenerationLength(1)
            .setDecryptionLength(1).build();
        try {
            keyManager.createChoiceCodeKeys(spec);
        } catch (KeyAlreadyExistsException | KeyNotFoundException e) {
            LOGGER.warn("Failed to create the choice code keys.", e);
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to create the choice code keys.", e);
        } catch (FingerprintGeneratorException e) {
            LOGGER.warn("Failed to generate hash of public key.", e);
		}
        return null;
    }

    private Void createElectionSigningKeys(TransactionStatus status) {
        ZonedDateTime from = ZonedDateTime.now();
        ZonedDateTime to = from.plusYears(1);
        try {
			keyManager.createElectionSigningKeys(electionEventId, from, to);
        } catch (KeyAlreadyExistsException e) {
            LOGGER.warn("Failed to create the election signing keys.", e);
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to create the election signing keys.", e);
        } catch (FingerprintGeneratorException e) {
             LOGGER.warn("Failed to generate hash of public key.", e);
		}
        return null;
    }

    private Void createMixingKeys(TransactionStatus status) {
        EncryptionParameters parameters =
            elGamalService.generateEncryptionParameters();
        MixingKeysSpec spec = new MixingKeysSpec.Builder()
            .setElectionEventId(electionEventId)
            .setElectoralAuthorityId(electoralAuthorityId)
            .setParameters(parameters).setLength(1).build();
        try {
            keyManager.createMixingKeys(spec);
        } catch (KeyAlreadyExistsException | KeyNotFoundException e) {
            LOGGER.warn("Failed to create the mixing keys.", e);
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to create the mixing keys.", e);
		} catch (FingerprintGeneratorException e) {
			LOGGER.warn("Failed to generate hash of public key.", e);
		}
        return null;
    }

    private Void printChoiceCodeKeys(TransactionStatus status) {
        try {
            System.err.println("Choice Code Generation Public Key: "
                + keyManager.getChoiceCodeGenerationPublicKey(
                    electionEventId, verificationCardSetId));
            System.err.println("Choice Code Decryption Public Key: "
                + keyManager.getChoiceCodeDecryptionPublicKey(
                    electionEventId, verificationCardSetId));
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to print the choice code keys.", e);
        }
        return null;
    }

    private Void printElectionSigningKeys(TransactionStatus status) {
        try {
            System.err
                .println("Election Signing Certificate: " + keyManager
                    .getElectionSigningCertificate(electionEventId));
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to print the election signing keys.", e);
        }
        return null;
    }

    private Void printMixingKeys(TransactionStatus status) {
        try {
            System.err.println(
                "Mixing Public Key: " + keyManager.getMixingPublicKey(
                    electionEventId, electoralAuthorityId));
        } catch (KeyManagementException e) {
            status.setRollbackOnly();
            LOGGER.warn("Failed to print the mixing keys.", e);
        }
        return null;
    }

    private Void printNodeKeys(TransactionStatus status) {
        System.err.println("CCN CA: " + keyManager.nodeCACertificate());
        System.err.println("Encryption Certificate: "
            + keyManager.nodeEncryptionCertificate());
        System.err.println("Log Signing Certificate: "
            + keyManager.nodeLogSigningCertificate());
        System.err.println("Log Encryption Certificate: "
            + keyManager.nodeLogEncryptionCertificate());
        return null;
    }
}
