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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.JobParametersIncrementer;
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.step.builder.StepBuilder;
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.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.validation.commons.beans.Constants;

@Configuration
@EnableBatchProcessing(modular = true)
@ComponentScan({"com.scytl.products.ov.sdm.decryption.spring.config"})
public class CommonBatchInfrastructure extends DefaultBatchConfigurer {

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

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

    @Autowired
    TransactionInfoProvider transactionInfoProvider;

    @Bean
    public JobBuilderFactory jobBuilderFactory() {
        return new JobBuilderFactory(getJobRepository());
    }

    @Bean
    public StepBuilderFactory stepBuilderFactory() {
        return new StepBuilderFactory(getJobRepository(), getTransactionManager());
    }

    public JobBuilder getJobBuilder(final String jobName, final JobParametersIncrementer incrementer,
                                    final JobExecutionListener jobExecutionListener) {
        JobBuilder jobBuilder = jobBuilderFactory().get(jobName).preventRestart();
        if( incrementer != null)
            jobBuilder = jobBuilder.incrementer(incrementer);
        if(jobExecutionListener != null)
            jobBuilder = jobBuilder.listener(jobExecutionListener);
        return jobBuilder;
    }

    public StepBuilder getStepBuilder(final String stepName) {
        return stepBuilderFactory().get(stepName);
    }

    @Override
    protected JobLauncher createJobLauncher() {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setTaskExecutor(jobExecutor());
        jobLauncher.setJobRepository(getJobRepository());
        return jobLauncher;
    }

    /**
     * TaskExecutor for executing the parallel flows and the parallel processing steps.
     * As you can see there we had to override the execute method. This is due to a Bug in Spring Batch
     * see https://jira.spring.io/browse/BATCH-2269
     * Since we need a InputQueue and OutputQueue per JobInstance we need to do this until the nex release (3.1.0)
     * On the other hand, this might be causing blocking problems when many jobs are submitted
     *
     * @return the taskExecutor
     */
    @Bean
    @JobScope
    public TaskExecutor stepExecutor(
        @Value("#{jobExecutionContext['" + Constants.JOB_TENANT_ID + "']}") final String tenantId,
        @Value("#{jobExecutionContext['" + Constants.JOB_APPROXIMATE_VOTE_COUNT+ "']}") final Integer approximateNumberVotes) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //avoid creating more threads than necessary. If the ballot box has a low number of votes, no need to create many
        //threads that will not be doing anything (start, return null, die)
        //start at least a minimum number of thread for the items to process (or available processors) and limit to
        //at most the number or available processors)
        int threadCount = Math.min(approximateNumberVotes, Runtime.getRuntime().availableProcessors());
        //for each job we are using at least 3 threads (one for each parallel flow). so we have to add at least those 3
        //to the number of threads  we will be using for processing to avoid blocking the job
        int maxThreadCount = threadCount + 3;
        executor.setCorePoolSize(maxThreadCount);
        LOGGER.info("Configured StepExecutor with [core={}, max={}] threads", maxThreadCount, maxThreadCount);
        String groupName = Thread.currentThread().getName().split("-")[1];
        executor.setThreadNamePrefix("StepExecutor-"+groupName+"-");
        executor.setTaskDecorator(new StepTaskDecorator(transactionInfoProvider, tenantId));
        return executor;
    }

    @Bean
    public TaskExecutor jobExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int threadCount = Math.min(Integer.parseInt(jobConcurrency), Runtime.getRuntime().availableProcessors());
        //if after 60 secs we don't get any new job, we start terminating idle threads. since this is a global threadpool
        //it will stay alive 'forever' unlike the step execution threadpool which is terminated after the job ends.
        executor.setAllowCoreThreadTimeOut(true);
        executor.setKeepAliveSeconds(60);
        executor.setCorePoolSize(threadCount);
        executor.setMaxPoolSize(threadCount);
        LOGGER.info("Configured JobExecutor with [core={}, max={}] threads", threadCount, threadCount);
        executor.setThreadNamePrefix("JobExecutor-");
        executor.setTaskDecorator(new JobTaskDecorator(transactionInfoProvider, "1"));
        return executor;
    }
}
