/*
 * 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.sdm.config.spring.batch;

import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.job.builder.FlowBuilder;
import org.springframework.batch.core.job.flow.Flow;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.item.ItemReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIPBKDFDeriver;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodec;
import com.scytl.products.ov.commons.verify.PayloadVerifier;
import com.scytl.products.ov.config.actions.ExtendedAuthenticationService;
import com.scytl.products.ov.config.commands.progress.ProgressManager;
import com.scytl.products.ov.config.commands.voters.JobExecutionObjectContext;
import com.scytl.products.ov.config.commands.voters.VotersGenerationTaskStaticContentProvider;
import com.scytl.products.ov.config.commands.voters.VotersParametersHolder;
import com.scytl.products.ov.config.commands.voters.VotersSerializationDestProvider;
import com.scytl.products.ov.config.commons.Constants;
import com.scytl.products.ov.config.model.authentication.AuthenticationKeyGenerator;
import com.scytl.products.ov.config.model.authentication.AuthenticationKeyGeneratorStrategyType;
import com.scytl.products.ov.config.model.authentication.ChallengeGenerator;
import com.scytl.products.ov.config.model.authentication.ChallengeGeneratorStrategyType;
import com.scytl.products.ov.config.model.authentication.service.AuthenticationGeneratorFactory;
import com.scytl.products.ov.config.model.authentication.service.AuthenticationKeyCryptoService;
import com.scytl.products.ov.config.model.authentication.service.ChallengeGeneratorFactory;
import com.scytl.products.ov.config.model.authentication.service.ChallengeService;
import com.scytl.products.ov.config.model.authentication.service.ChallengeServiceAPI;
import com.scytl.products.ov.config.model.authentication.service.ProvidedChallengeSource;
import com.scytl.products.ov.config.model.authentication.service.SequentialProvidedChallengeSource;
import com.scytl.products.ov.config.model.authentication.service.StartVotingKeyService;
import com.scytl.products.ov.config.spring.SecureLoggingConfig;
import com.scytl.products.ov.config.spring.batch.CommonBatchInfrastructure;
import com.scytl.products.ov.datapacks.beans.VerificationCardIdComputedValues;
import com.scytl.products.ov.sdm.config.spring.batch.listeners.VotingCardGeneratedOutputWriterListener;
import com.scytl.products.ov.sdm.config.spring.batch.listeners.VotingCardGenerationJobListener;
import com.scytl.products.ov.sdm.config.spring.batch.listeners.VotingCardGenerationStepListener;
import com.scytl.products.ov.sdm.config.spring.batch.readers.ComputedValuesReader;
import com.scytl.products.ov.sdm.config.spring.batch.readers.OutputQueueReader;
import com.scytl.products.ov.sdm.config.spring.batch.writers.CodesMappingTableWriter;
import com.scytl.products.ov.sdm.config.spring.batch.writers.CompositeOutputWriter;
import com.scytl.products.ov.sdm.config.spring.batch.writers.CredentialDataWriter;
import com.scytl.products.ov.sdm.config.spring.batch.writers.ExtendedAuthenticationWriter;
import com.scytl.products.ov.sdm.config.spring.batch.writers.OutputQueueWriter;
import com.scytl.products.ov.sdm.config.spring.batch.writers.VerificationCardDataWriter;
import com.scytl.products.ov.sdm.config.spring.batch.writers.VoterInformationWriter;
import com.scytl.products.ov.utils.FingerprintGenerator;

/**
 * Configuration class of voting card generation job for product
 */
@Configuration("configuration-job-config-product")
public class ConfigJobConfigProduct {

    @Value("${spring.batch.steps.concurrency:4}")
    private String stepConcurrency;

    @Autowired
    Environment env;

    private static final String STR_REPLACED_AT_RUNTIME = null;

    private static final int INT_REPLACED_AT_RUNTIME = -1;

    @Autowired
    private FingerprintGenerator fingerprintGenerator;

    @Autowired
    private JobExecutionObjectContext objectContext;

    @Autowired
    private SecureLoggingConfig loggingConfig;

    @Autowired
    private CommonBatchInfrastructure commonBatchInfrastructure;

    @Autowired
    private ProgressManager progressManager;

    @Autowired
    PrimitivesServiceAPI primitivesService;

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    ElGamalComputationsValuesCodec codec;

    @Autowired
    PayloadVerifier payloadVerifier;

    @Bean
    Job job() {

        /**
         * This is an attempt at trying to define a 'template' for job
         * configuration Example: pre-processing flow -> processing flow ->
         * post-processing flow each impl adds the steps it wants to the flow
         * before the main flow. can be used to setup the context and other
         * stuff the main flow may need . each impl adds the steps or flows it
         * wants to perform after the main processing. can be cleanup or other
         * tasks like signing files, etc...
         */
        return commonBatchInfrastructure
            .getJobBuilder(Constants.JOB_NAME_PREFIX + "-product", new RunIdIncrementer(), jobExecutionListener())
            .start(preProcessingFlow().build()) //
            .next(verificationAndCombinationFlow().build()) //
            .next(generationFlow().build()) //
            .next(postProcessingFlow().build()) //
            .end().build();
    }

    protected FlowBuilder<Flow> verificationAndCombinationFlow() {
        return new FlowBuilder<Flow>("verificationAndCombination").from(verificationAndCombinationStep());
    }

    @Bean
    public Step verificationAndCombinationStep() {
        return commonBatchInfrastructure.getStepBuilder("verificationAndCombinationStep")
            .tasklet(verificationAndCombinationTasklet(STR_REPLACED_AT_RUNTIME, STR_REPLACED_AT_RUNTIME, null)).build();
    }

    @Bean
    @JobScope
    public Tasklet verificationAndCombinationTasklet(
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_BASE_PATH + "']}") final String basePath,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_PLATFORM_ROOT_CA_CERTIFICATE
                + "']}") final String platformRootCACertificate,
            @Value("#{jobExecution}") final JobExecution jobExecution) {

		final VotingCardGenerationJobExecutionContext jobExecutionContext = new VotingCardGenerationJobExecutionContext(
				jobExecution.getExecutionContext());
		final VotersParametersHolder holder = objectContext.get(jobExecutionContext.getJobInstanceId(),
				VotersParametersHolder.class);

        X509Certificate certificate;
        try {
            certificate = (X509Certificate) PemUtils.certificateFromPem(platformRootCACertificate);
        } catch (GeneralCryptoLibException e) {
            throw new IllegalStateException("Error obtaining platform root certificate", e);
        }

        return new NodeContributionsVerificationCombinationTasklet(basePath, commonBatchInfrastructure,
            holder.getEncryptionParameters(), combinationOutputQueue(), objectMapper, payloadVerifier, certificate,
            codec);
    }
    
    protected FlowBuilder<Flow> postProcessingFlow() {
        return new FlowBuilder<Flow>("endFlow").start(writeVerificationDataStep());
    }

    protected FlowBuilder<Flow> generationFlow() {

        Flow generationFlow = new FlowBuilder<Flow>("generationFlow").start(generateVotingCardStep()).end();

        Flow writingFlow = new FlowBuilder<Flow>("writingFlow").start(writeOutputStep()).end();

        return new FlowBuilder<Flow>("processingFlow")
            .split(commonBatchInfrastructure.stepExecutor(STR_REPLACED_AT_RUNTIME, INT_REPLACED_AT_RUNTIME))
            .add(generationFlow, writingFlow);
    }

    protected FlowBuilder<Flow> preProcessingFlow() {
        return new FlowBuilder<Flow>("preparation").from(prepareJobExecutionContextStep())
            .next(prepareVotingCardGenerationDataStep());
    }

    @Bean
    public JobExecutionListener jobExecutionListener() {
        return new VotingCardGenerationJobListener();
    }

    /**
     * This step loads all input data received from the request, necessary for
     * voting card generation, in to the spring batch job execution context
     *
     * @return the step
     */
    @Bean
    public Step prepareJobExecutionContextStep() {
        return commonBatchInfrastructure.getStepBuilder("prepareExecutionContext")
            .tasklet(prepareJobExecutionContextTasklet()).build();
    }

    @Bean
    public Tasklet prepareJobExecutionContextTasklet() {
        return new PrepareJobExecutionContextTasklet();
    }

    /**
     * This step creates and stores in a 'cache' bean some extra complex classes
     * needed for the voting card generation that are not possible (or easy) to
     * store in the standard spring batch job execution context.
     *
     * @return the step
     */
    @Bean
    public Step prepareVotingCardGenerationDataStep() {
        return commonBatchInfrastructure.getStepBuilder("prepareVotingCardGenerationDataStep")
            .tasklet(prepareVotingCardGenerationDataTasklet()).build();
    }

    @Bean
    public Tasklet prepareVotingCardGenerationDataTasklet() {
        return new PrepareVotingCardGenerationDataTasklet();
    }

    /**
     * This step generates the voting cards and other associated data one by one
     * and puts them in a queue used as a buffer for writing to the various
     * output files.
     *
     * @return the step
     */
    @Bean
    public Step generateVotingCardStep() {
        return commonBatchInfrastructure.getStepBuilder("generateVotingCardStep")
            .<VerificationCardIdComputedValues, GeneratedVotingCardOutput> chunk(1).reader(computedValuesReader())
            .processor(votingCardGenerator(null)).writer(outputQueueWriter())
            .taskExecutor(commonBatchInfrastructure.stepExecutor(STR_REPLACED_AT_RUNTIME, INT_REPLACED_AT_RUNTIME))
            .throttleLimit(Integer.valueOf(stepConcurrency)).listener(stepExecutionListener()).build();
    }

    @Bean
    @JobScope
    ItemReader<VerificationCardIdComputedValues> computedValuesReader() {
        return new ComputedValuesReader(combinationOutputQueue());
    }

    @Bean
    @JobScope
    StepExecutionListener stepExecutionListener() {
        return new VotingCardGenerationStepListener(generationOutputQueue());
    }

    @Bean
    public Step writeOutputStep() {
        return commonBatchInfrastructure.getStepBuilder("writeOutputStep")
            .<GeneratedVotingCardOutput, GeneratedVotingCardOutput> chunk(1).reader(generationOutputQueueReader())
            .writer(compositeOutputWriter(STR_REPLACED_AT_RUNTIME, STR_REPLACED_AT_RUNTIME, STR_REPLACED_AT_RUNTIME))
            .listener(votingCardGeneratedOutputWriterListener(STR_REPLACED_AT_RUNTIME, null)).build();
    }

    @Bean
    public Step writeVerificationDataStep() {
        return commonBatchInfrastructure.getStepBuilder("writeVerificationData")
            .tasklet(
                writeVerificationDataTasklet(STR_REPLACED_AT_RUNTIME, STR_REPLACED_AT_RUNTIME, STR_REPLACED_AT_RUNTIME))
            .build();
    }

    @Bean
    @JobScope
    Tasklet writeVerificationDataTasklet(
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_BASE_PATH + "']}") final String outputPath,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_VOTINGCARDSET_ID
                + "']}") final String votingCardSetId,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_VERIFICATIONCARDSET_ID
                + "']}") final String verificationCardSetId) {

        final VotersSerializationDestProvider destProvider =
            commonBatchInfrastructure.getDataSerializationProvider(outputPath, votingCardSetId, verificationCardSetId);
        return new WriteVerificationDataTasklet(destProvider, objectContext);
    }

    @Bean
    @JobScope
    OutputQueueWriter outputQueueWriter() {
        return new OutputQueueWriter(generationOutputQueue());
    }

    @Bean
    @JobScope
    VotingCardGenerator votingCardGenerator(@Value("#{jobExecution}") final JobExecution jobExecution) {

        final CryptoAPIPBKDFDeriver pbkdfDeriver = primitivesService.getPBKDFDeriver();
        final VotingCardGenerationJobExecutionContext jobExecutionContext =
            new VotingCardGenerationJobExecutionContext(jobExecution.getExecutionContext());
        final VotersParametersHolder holder =
            objectContext.get(jobExecutionContext.getJobInstanceId(), VotersParametersHolder.class);
        final VotersGenerationTaskStaticContentProvider staticContentProvider =
            new VotersGenerationTaskStaticContentProvider(holder.getEncryptionParameters(), holder);

        return new VotingCardGenerator(holder, jobExecutionContext, pbkdfDeriver, staticContentProvider);
    }

    @Bean
    @JobScope
    VotingCardGeneratedOutputWriterListener votingCardGeneratedOutputWriterListener(@Value("#{jobExecutionContext['"
        + Constants.JOB_IN_PARAM_JOB_INSTANCE_ID + "']}") final String jobInstanceId,
            @Value("#{jobExecution}") final JobExecution jobExecution) {

        return new VotingCardGeneratedOutputWriterListener(UUID.fromString(jobInstanceId),
            jobExecution.getExecutionContext(), loggingConfig.secureLoggingWriter(), progressManager);
    }

    @Bean
    @JobScope
    CompositeOutputWriter compositeOutputWriter(
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_BASE_PATH + "']}") final String basePath,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_VOTINGCARDSET_ID
                + "']}") final String votingCardSetId,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_VERIFICATIONCARDSET_ID
                + "']}") final String verificationCardSetId) {

        final CompositeOutputWriter writer = new CompositeOutputWriter();
        writer.setDelegates(Arrays.asList(voterInformationWriter(basePath, votingCardSetId, verificationCardSetId),
            credentialDataWriter(basePath, votingCardSetId, verificationCardSetId),
            codesMappingTableWriter(basePath, votingCardSetId, verificationCardSetId),
            verificationCardDataWriter(basePath, votingCardSetId, verificationCardSetId),
            extendedAuthenticationWriter(basePath, votingCardSetId, verificationCardSetId)));
        return writer;
    }

    VerificationCardDataWriter verificationCardDataWriter(final String basePath, final String votingCardSetId,
            final String verificationCardSetId) {
        final Path path = commonBatchInfrastructure
            .getDataSerializationProvider(basePath, votingCardSetId, verificationCardSetId).getVerificationCardData();
        return new VerificationCardDataWriter(path, fingerprintGenerator, loggingConfig.defaultLoggingWriter());
    }

    ExtendedAuthenticationWriter extendedAuthenticationWriter(final String basePath, final String votingCardSetId,
            final String verificationCardSetId) {
        final Path path = commonBatchInfrastructure
            .getDataSerializationProvider(basePath, votingCardSetId, verificationCardSetId).getTempExtendedAuth("");
        return new ExtendedAuthenticationWriter(path);
    }

    CodesMappingTableWriter codesMappingTableWriter(final String basePath, final String votingCardSetId,
            final String verificationCardSetId) {
        final Path path =
            commonBatchInfrastructure.getDataSerializationProvider(basePath, votingCardSetId, verificationCardSetId)
                .getCodesMappingTablesContextData();
        return new CodesMappingTableWriter(path);
    }

    CredentialDataWriter credentialDataWriter(final String basePath, final String votingCardSetId,
            final String verificationCardSetId) {
        final Path path = commonBatchInfrastructure
            .getDataSerializationProvider(basePath, votingCardSetId, verificationCardSetId).getCredentialsData();
        return new CredentialDataWriter(path, fingerprintGenerator, loggingConfig.defaultLoggingWriter());
    }

    VoterInformationWriter voterInformationWriter(final String basePath, final String votingCardSetId,
            final String verificationCardSetId) {
        final Path path = commonBatchInfrastructure
            .getDataSerializationProvider(basePath, votingCardSetId, verificationCardSetId).getVoterInformation();
        return new VoterInformationWriter(path);
    }

    @Bean
    @JobScope
    BlockingQueue<GeneratedVotingCardOutput> generationOutputQueue() {
        return new LinkedBlockingQueue<>();
    }

    @Bean
    @JobScope
    OutputQueueReader generationOutputQueueReader() {
        return new OutputQueueReader(generationOutputQueue());
    }

    @Bean
    @JobScope
    BlockingQueue<VerificationCardIdComputedValues> combinationOutputQueue() {
        return new LinkedBlockingQueue<>();
    }

    @Bean
    @JobScope
    public ProvidedChallengeSource providedChallengeSource(
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_BASE_PATH + "']}") final String outputPath,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_VOTINGCARDSET_ID
                + "']}") final String votingCardSetId,
            @Value("#{jobExecutionContext['" + Constants.JOB_IN_PARAM_VERIFICATIONCARDSET_ID
                + "']}") final String verificationCardSetId) {

        final Path providedChallengePath = commonBatchInfrastructure
            .getDataSerializationProvider(outputPath, votingCardSetId, verificationCardSetId).getProvidedChallenge();
        return new SequentialProvidedChallengeSource(providedChallengePath);
    }

    @Bean
    @JobScope
    AuthenticationKeyCryptoService authKeyService() {
        return new AuthenticationKeyCryptoService();
    }

    @Bean
    @JobScope
    ChallengeGeneratorFactory challengeGeneratorFactory() {
        return new ChallengeGeneratorFactory();
    }

    @Bean
    @JobScope
    ChallengeGenerator challengeGenerator(final ChallengeGeneratorFactory challengeGeneratorFactory) {
        String property = env.getProperty("challenge.generator.type");
        ChallengeGeneratorStrategyType challengeGeneratorStrategyType =
            ChallengeGeneratorStrategyType.valueOf(property);
        return challengeGeneratorFactory.createStrategy(challengeGeneratorStrategyType);
    }

    @Bean
    @JobScope
    ChallengeServiceAPI challengeService(final PrimitivesServiceAPI primitivesService,
            final ChallengeGenerator challengeGenerator) {
        return new ChallengeService(primitivesService, challengeGenerator);
    }

    @Bean
    @JobScope
    AuthenticationGeneratorFactory authenticationGeneratorFactory() {
        return new AuthenticationGeneratorFactory();
    }

    @Bean
    @JobScope
    AuthenticationKeyGenerator authenticationKeyGenerator(
            final AuthenticationGeneratorFactory authenticationGeneratorFactory) {
        String property = env.getProperty("auth.generator.type");
        AuthenticationKeyGeneratorStrategyType authenticationKeyGeneratorStrategyType =
            AuthenticationKeyGeneratorStrategyType.valueOf(property);
        return authenticationGeneratorFactory.createStrategy(authenticationKeyGeneratorStrategyType);
    }

    @Bean
    @JobScope
    ExtendedAuthenticationService createAndHandleAuthKey(final AuthenticationKeyCryptoService authKeyService,
            final AuthenticationKeyGenerator authenticationKeyGenerator, final ChallengeServiceAPI challengeService) {
        return new ExtendedAuthenticationService(authKeyService, authenticationKeyGenerator, challengeService);
    }

    @Bean
    @JobScope
    StartVotingKeyService startVotingKeyService(final AuthenticationKeyGenerator authenticationKeyGenerator) {
        return new StartVotingKeyService(authenticationKeyGenerator);
    }
}
