/*
 * 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
 */
/**
 * Created by jruiz on 25/08/15.
 */
package com.scytl.products.ov.config.commands.api;

import static com.scytl.products.ov.config.commons.Constants.CHOICE_CODES_KEY_DELIMITER;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_BALLOTBOX_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_BALLOT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_ELECTIONEVENT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_ELECTORALAUTHORITY_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_JOB_INSTANCE_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_KEY_FOR_PROTECTING_KEYSTORE_PW;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_NUMBER_VOTING_CARDS;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_TENANT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VALIDITY_PERIOD_END;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VALIDITY_PERIOD_START;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VERIFICATIONCARDSET_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VOTINGCARDSET_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VOTING_CARD_SET_NAME;
import static com.scytl.products.ov.config.commons.Constants.JOB_OUT_PARAM_GENERATED_VC_COUNT;
import static com.scytl.products.ov.config.logevents.ConfigGeneratorLogEvents.GENVCD_ERROR_GENERATING_VERIFICATION_CARD_IDS;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionException;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;

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.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.config.commands.api.output.BallotBoxesServiceOutput;
import com.scytl.products.ov.config.commands.api.output.ElectionEventServiceOutput;
import com.scytl.products.ov.config.commands.api.output.ProgressOutput;
import com.scytl.products.ov.config.commands.ballotbox.BallotBoxGenerator;
import com.scytl.products.ov.config.commands.ballotbox.BallotBoxHolderInitializer;
import com.scytl.products.ov.config.commands.ballotbox.BallotBoxParametersHolder;
import com.scytl.products.ov.config.commands.electionevent.CreateElectionEventGenerator;
import com.scytl.products.ov.config.commands.electionevent.CreateElectionEventHolderInitializer;
import com.scytl.products.ov.config.commands.electionevent.CreateElectionEventOutput;
import com.scytl.products.ov.config.commands.electionevent.CreateElectionEventParametersHolder;
import com.scytl.products.ov.config.commands.electionevent.CreateElectionEventSerializer;
import com.scytl.products.ov.config.commands.progress.ProgressManager;
import com.scytl.products.ov.config.commands.progress.ProgressManager_old;
import com.scytl.products.ov.config.commands.uuid.UUIDGenerator;
import com.scytl.products.ov.config.commands.voters.JobExecutionObjectContext;
import com.scytl.products.ov.config.commands.voters.JobSelectionStrategy;
import com.scytl.products.ov.config.commands.voters.VotersParametersHolder;
import com.scytl.products.ov.config.commons.Constants;
import com.scytl.products.ov.config.commons.beans.StartVotingCardGenerationJobResponse;
import com.scytl.products.ov.config.commons.beans.VotingCardGenerationJobStatus;
import com.scytl.products.ov.config.commons.beans.spring.batch.SensitiveAwareJobParametersBuilder;
import com.scytl.products.ov.config.commons.progress.JobProgressDetails;
import com.scytl.products.ov.config.exceptions.ConfigurationEngineException;
import com.scytl.products.ov.config.exceptions.CreateBallotBoxesException;
import com.scytl.products.ov.config.exceptions.CreateElectionEventException;
import com.scytl.products.ov.config.exceptions.specific.GenerateVerificationCardDataException;
import com.scytl.products.ov.config.exceptions.specific.VerificationCardIdsGenerationException;
import com.scytl.products.ov.config.logevents.ConfigGeneratorLogEvents;
import com.scytl.products.ov.config.logs.Loggable;

public class ConfigurationService {

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

    /** The length of a generated verification card ID */
    public static final int VERIFICATION_CARD_ID_SIZE = new UUIDGenerator().generate().length();

    @Loggable
    private LoggingWriter logWriter;

    @Autowired
    private CreateElectionEventGenerator createElectionEventGenerator;

    @Autowired
    private CreateElectionEventSerializer createElectionEventSerializer;

    @Autowired
    private BallotBoxGenerator ballotBoxGenerator;

    @Autowired
    private BallotBoxHolderInitializer ballotBoxHolderInitializer;

    @Autowired
    private CreateElectionEventHolderInitializer electionEventHolderInitializer;

    @Autowired
    private UUIDGenerator uuidGenerator;

    @Autowired
    private ProgressManager_old voterVotersProgressManager;

    @Autowired
    private JobExecutionObjectContext jobExecutionObjectContext;

    @Autowired
    private ProgressManager progressManager;

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private JobExplorer jobExplorer;

    @Autowired
    private JobSelectionStrategy jobSelectionStrategy;

    @Autowired
    private JobRegistry jobRegistry;

    @Autowired
    private TransactionInfoProvider transactionInfoProvider;

    public ElectionEventServiceOutput createElectionEvent(CreateElectionEventParametersHolder holder)
            throws CreateElectionEventException {

        try {
            LOG.info("Loading internal configuration...");

            try (InputStream is = getKeysConfiguration()) {
                electionEventHolderInitializer.init(holder, is);
            }

            LOG.info("Generating Election Event...");

            final CreateElectionEventOutput createElectionEventOutput = createElectionEventGenerator.generate(holder);
            try {
                LOG.info("Processing the output...");

                createElectionEventSerializer.serialize(holder, createElectionEventOutput);
            } finally {
                createElectionEventOutput.clearPasswords();
            }

            LOG.info("The generation of Election Event finished correctly. It can be found in:");
            LOG.info("\t" + holder.getOfflineFolder().toAbsolutePath().toString());
            LOG.info("\t" + holder.getOnlineAuthenticationFolder().toAbsolutePath().toString());
            LOG.info("\t" + holder.getOnlineElectionInformationFolder().toAbsolutePath().toString());

            ElectionEventServiceOutput electionEventOutput = new ElectionEventServiceOutput();
            electionEventOutput.setOfflineFolder(holder.getOfflineFolder().toAbsolutePath().toString());
            electionEventOutput
                .setOnlineAuthenticationFolder(holder.getOnlineAuthenticationFolder().toAbsolutePath().toString());
            electionEventOutput.setOnlineElectionInformationFolder(
                holder.getOnlineElectionInformationFolder().toAbsolutePath().toString());

            return electionEventOutput;

        } catch (Exception e) {
            throw new CreateElectionEventException(e);
        }
    }

    public BallotBoxesServiceOutput createBallotBoxes(BallotBoxParametersHolder ballotBoxHolder)
            throws CreateBallotBoxesException {

        try {
            LOG.info("Loading required certificates and private keys...");

            try (InputStream is = getKeysConfiguration()) {
                ballotBoxHolderInitializer.init(ballotBoxHolder, is);
            }

            LOG.info("Creating ballot boxes...");

            final BallotBoxesServiceOutput boxesServiceOutput = ballotBoxGenerator.generate(ballotBoxHolder);

            LOG.info("The ballot boxes were successfully created. They can be found in:");
            LOG.info("\t" + boxesServiceOutput.getOutputPath());

            return boxesServiceOutput;

        } catch (Exception e) {
            throw new CreateBallotBoxesException(e);
        }
    }

    /**
     * @deprecated
     */
    @Deprecated
    public ProgressOutput getProgress() {

        ProgressOutput progressOutput = new ProgressOutput();
        progressOutput.setPercentage(voterVotersProgressManager.getPercentage().intValue());
        return progressOutput;
    }


    /**
     * @deprecated
     */
    @Deprecated
    public ProgressOutput getProgress(final String votingCardSetId) {
        ProgressOutput progressOutput = new ProgressOutput();
        AtomicInteger percentage = voterVotersProgressManager.getPercentage(votingCardSetId);
        AtomicLong remainingTime = voterVotersProgressManager.getRemainingTime(votingCardSetId);
        if (percentage == null) {
            percentage = new AtomicInteger(0);
        }
        if (remainingTime == null) {
            remainingTime = new AtomicLong(0);
        }
        progressOutput.setPercentage(percentage.intValue());
        progressOutput.setRemainingTime(remainingTime.longValue());
        return progressOutput;
    }

    /**
     * Get job status for the specified job
     *
     * @param tenantId
     *            the tenant
     * @param electionEventId
     *            the election event
     * @param jobId
     *            the job instance id
     * @return job status for the specified job
     */
    public Optional<VotingCardGenerationJobStatus> getVotingCardGenerationJobStatus(final String tenantId,
            final String electionEventId, final String jobId) {

        final Function<JobExecution, VotingCardGenerationJobStatus> mapToJobStatus = mapJobExecutionToJobStatus();
        // why jobParameters? well we might get a request before we have
        // completed running the "job preparation task"
        // which is where we populate the jobExecution context.
        // until there the execution context is empty and this may fail
        final Predicate<JobExecution> filterByJobInstance =
            jobExecution -> jobExecution.getJobParameters().getString(JOB_IN_PARAM_TENANT_ID).equalsIgnoreCase(tenantId)
                && jobExecution.getJobParameters().getString(JOB_IN_PARAM_ELECTIONEVENT_ID)
                    .equalsIgnoreCase(electionEventId)
                && jobExecution.getJobParameters().getString(JOB_IN_PARAM_JOB_INSTANCE_ID).equalsIgnoreCase(jobId);

        final List<JobInstance> jobInstances =
            jobExplorer.findJobInstancesByJobName("votingcardset-generation-*", 0, Integer.MAX_VALUE);
        return jobInstances.stream().map(jobExplorer::getJobExecutions).flatMap(Collection::stream)
            .filter(filterByJobInstance).map(mapToJobStatus).findAny();
    }

    private Function<JobExecution, VotingCardGenerationJobStatus> mapJobExecutionToJobStatus() {
        return je -> {
            if (BatchStatus.UNKNOWN.equals(je.getStatus())) {
                return VotingCardGenerationJobStatus.UNKNOWN;
            } else {
                ExecutionContext executionContext = je.getExecutionContext();
                // why jobParameters for the id? well we might get a request
                // before we have completed running the
                // "job preparation task" which is where we populate the
                // jobExecution context.
                // until there the execution context is empty and this will
                // return null/empty which blow up getting
                // the progress details (i really should make this more
                // resilient)
                final String id = je.getJobParameters().getString(JOB_IN_PARAM_JOB_INSTANCE_ID);
                final String status = je.getStatus().toString();
                final String statusDetails =
                    je.getStatus().isUnsuccessful() ? je.getExitStatus().getExitDescription() : null;
                // start time may be null if the job hasn't been started yet
                final Instant startTime =
                    Optional.ofNullable(je.getStartTime()).orElse(Date.from(Instant.EPOCH)).toInstant();
                final Optional<JobProgressDetails> details = progressManager.getJobProgress(UUID.fromString(id));
                final String verificationCardSetId =
                    executionContext.getString(JOB_IN_PARAM_VERIFICATIONCARDSET_ID, null);
                final int votingCardCount = executionContext.getInt(JOB_OUT_PARAM_GENERATED_VC_COUNT, 0);
                final int errorCount = executionContext.getInt(Constants.JOB_OUT_PARAM_ERROR_COUNT, 0);
                return new VotingCardGenerationJobStatus(UUID.fromString(id), status, startTime, statusDetails,
                    details.orElse(null), verificationCardSetId, votingCardCount, errorCount);
            }
        };
    }

    /**
     * Get list of all Voting Card Generation jobs for the specified tenant and electionEvent with a specific status
     *
     * @param tenantId
     *            the tenant
     * @param electionEventId
     *            the election event
     * @param jobStatus
     *            the job status
     * @return list of all Voting Card Generation jobs for the specified tenant and electionEvent
     */
    public List<VotingCardGenerationJobStatus> getJobsWithStatus(final String tenantId, final String electionEventId,
            final String jobStatus) {
        final Function<JobExecution, VotingCardGenerationJobStatus> mapToJobStatus = mapJobExecutionToJobStatus();
        final Predicate<JobExecution> filterWithStatus =
            jobExecution -> jobExecution.getJobParameters().getString(JOB_IN_PARAM_TENANT_ID).equalsIgnoreCase(tenantId)
                && jobExecution.getJobParameters().getString(JOB_IN_PARAM_ELECTIONEVENT_ID).equalsIgnoreCase(
                    electionEventId)
                && BatchStatus.valueOf(jobStatus.toUpperCase()).equals(jobExecution.getStatus());

        final List<JobInstance> jobInstances =
            jobExplorer.findJobInstancesByJobName("votingcardset-generation-*", 0, Integer.MAX_VALUE);
        final List<VotingCardGenerationJobStatus> jobs = jobInstances.stream().map(jobExplorer::getJobExecutions)
            .flatMap(Collection::stream).filter(filterWithStatus).map(mapToJobStatus).collect(Collectors.toList());

        return jobs;
    }

    /**
     * Get list of all Voting Card Generation jobs for the specified tenant and electionEvent NOTE: we are actively
     * ignoring the tenant and election event parameters since the way the SDM BE is designed makes it difficult to
     * provide us with this parameters
     * 
     * @return list of all Voting Card Generation jobs for the specified tenant and electionEvent
     */
    public List<VotingCardGenerationJobStatus> getJobs() {
        final Function<JobExecution, VotingCardGenerationJobStatus> mapToJobStatus = mapJobExecutionToJobStatus();

        // we're using an in-memory repository, so this is fast. if we ever
        // change to a slower implementation, we
        // should optimize this.
        // also since this is in-memory, we should evict 'old' jobs so we don't
        // end-up with too much memory used.
        final List<JobInstance> jobInstances =
            jobExplorer.findJobInstancesByJobName("votingcardset-generation-*", 0, Integer.MAX_VALUE);
        // one way of having a count at the end of the stream
        final List<VotingCardGenerationJobStatus> jobs = jobInstances.stream().map(jobExplorer::getJobExecutions)
            .flatMap(Collection::stream).map(mapToJobStatus).collect(Collectors.toList());

        return jobs;
    }

    /**
     * Start a new voting card generation Spring Batch job with the specified input parameters.
     *
     * @param holder
     *            holder class for all the needed job parameters
     * @return object with the initial status of the job
     */
    public StartVotingCardGenerationJobResponse startVotingCardGenerationJob(final String tenantId,
            final String electionEventId, final VotersParametersHolder holder) {

        // avoid error in SplunkFormatter due to missing tracking id
        transactionInfoProvider.generate(tenantId, "", "");

        JobParameters jobParams = prepareJobParameters(tenantId, electionEventId, holder);
        final String jobInstanceId = jobParams.getString(JOB_IN_PARAM_JOB_INSTANCE_ID);
        jobExecutionObjectContext.put(jobInstanceId, holder, VotersParametersHolder.class);

        final UUID jobId = UUID.fromString(jobInstanceId);
        try {
            final String jobQualifier = jobSelectionStrategy.select();
            final Job job = jobRegistry.getJob(jobQualifier);
            final JobExecution jobExecution = jobLauncher.run(job, jobParams);

            final String jobStatus = jobExecution.getStatus().toString();
            final Instant created = jobExecution.getCreateTime().toInstant();

            final Long numberOfVotingCards = Long.parseLong(jobParams.getString(JOB_IN_PARAM_NUMBER_VOTING_CARDS));
            progressManager.registerJob(jobId, new JobProgressDetails(jobId, numberOfVotingCards));

            return new StartVotingCardGenerationJobResponse(jobInstanceId, jobStatus, created);
        } catch (JobExecutionException e) {
            // in case we registered the job, remove it.
            progressManager.unregisterJob(jobId);
            throw new ConfigurationEngineException(e);
        }
    }

    /**
     * Generate the number of verificationCardIds specified by parameter.
     * 
     * @return a stream of the requested number of verification cards IDs.
     */
    public Stream<String> createVerificationCardIdStream(final String electionEventId,
            final String verificationCardSetId, final int numberOfVerificationCardIdsToGenerate)
            throws VerificationCardIdsGenerationException {
        return IntStream.range(0, numberOfVerificationCardIdsToGenerate)
            .mapToObj(i -> generateVerificationCardId(verificationCardSetId, electionEventId,
                numberOfVerificationCardIdsToGenerate));
    }

    /**
     * Generate a verification card id
     */
    private String generateVerificationCardId(final String verificationCardSetId, final String electionEventId,
            final int numberOfVerificationCards) {
        // Because it runs in a different thread.
        transactionInfoProvider.generate(null, null, null);

        String verificationCardId;
        try {
            verificationCardId = uuidGenerator.generate();
            logWriter.log(Level.DEBUG, new LogContent.LogContentBuilder()
                .logEvent(ConfigGeneratorLogEvents.GENVCD_SUCCESS_GENERATING_VERIFICATION_CARD_IDS).user("adminID")
                .electionEvent(electionEventId).additionalInfo("verifcs_id", verificationCardSetId).createLogInfo());
        } catch (RuntimeException e) {
            logWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(ConfigGeneratorLogEvents.GENVCD_ERROR_GENERATING_VERIFICATION_CARD_IDS).user("adminID")
                    .electionEvent(electionEventId).objectId(verificationCardSetId)
                    .additionalInfo("verifcs_id", verificationCardSetId)
                    .additionalInfo("vcs_size", Integer.toString(numberOfVerificationCards))
                    .additionalInfo("err_desc", e.getMessage()).createLogInfo());
            throw new GenerateVerificationCardDataException(GENVCD_ERROR_GENERATING_VERIFICATION_CARD_IDS.getInfo(), e);
        }
        return verificationCardId;
    }

    private JobParameters prepareJobParameters(final String tenantId, final String electionEventId,
            final VotersParametersHolder input) {

        final SensitiveAwareJobParametersBuilder jobParametersBuilder = new SensitiveAwareJobParametersBuilder();
        jobParametersBuilder.addString(JOB_IN_PARAM_JOB_INSTANCE_ID, UUID.randomUUID().toString(), true);
        jobParametersBuilder.addString(JOB_IN_PARAM_TENANT_ID, tenantId, true);
        jobParametersBuilder.addString(JOB_IN_PARAM_ELECTIONEVENT_ID, electionEventId, true);
        jobParametersBuilder.addString(JOB_IN_PARAM_BALLOTBOX_ID, input.getBallotBoxID(), true);
        jobParametersBuilder.addString(JOB_IN_PARAM_BALLOT_ID, input.getBallotID());
        jobParametersBuilder.addString(JOB_IN_PARAM_ELECTORALAUTHORITY_ID, input.getElectoralAuthorityID());
        jobParametersBuilder.addString(JOB_IN_PARAM_VOTINGCARDSET_ID, input.getVotingCardSetID());
        jobParametersBuilder.addString(JOB_IN_PARAM_VERIFICATIONCARDSET_ID, input.getVerificationCardSetID());
        jobParametersBuilder.addString(JOB_IN_PARAM_NUMBER_VOTING_CARDS,
            Integer.toString(input.getNumberVotingCards()));
        jobParametersBuilder.addString(JOB_IN_PARAM_VOTING_CARD_SET_NAME, input.getVotingCardSetAlias());
        jobParametersBuilder.addString(JOB_IN_PARAM_VALIDITY_PERIOD_START,
            input.getCertificatesStartValidityPeriod().toString());
        jobParametersBuilder.addString(JOB_IN_PARAM_VALIDITY_PERIOD_END,
            input.getCertificatesEndValidityPeriod().toString());
        jobParametersBuilder.addSensitiveString(JOB_IN_PARAM_KEY_FOR_PROTECTING_KEYSTORE_PW,
            input.getKeyForProtectingKeystorePassword());
        jobParametersBuilder.addString(Constants.JOB_IN_PARAM_BASE_PATH, input.getAbsoluteBasePath().toString());

        String choiceCodeEncryptionKeyAsString =
            String.join(CHOICE_CODES_KEY_DELIMITER, input.getChoiceCodesEncryptionKey());
        jobParametersBuilder.addString(Constants.JOB_IN_PARAM_CHOICE_CODES_ENCRYPTION_KEY,
            choiceCodeEncryptionKeyAsString);

        jobParametersBuilder.addSensitiveString(Constants.JOB_IN_PARAM_PRIME_ENCRYPTION_PRIVATE_KEY_JSON,
            input.getPrimeEncryptionPrivateKeyJson());

        jobParametersBuilder.addString(Constants.JOB_IN_PARAM_PLATFORM_ROOT_CA_CERTIFICATE,
            input.getPlatformRootCACertificate());

        return jobParametersBuilder.toJobParameters();
    }

    private InputStream getKeysConfiguration() throws FileNotFoundException {
        // get inputstream to keys_config.json from classpath
        String resourceName = "/" + com.scytl.products.ov.constants.Constants.KEYS_CONFIG_FILENAME;
        final InputStream resourceAsStream = this.getClass().getResourceAsStream(resourceName);
        if (resourceAsStream == null) {
            throw new FileNotFoundException(
                String.format("Resource file '%s' was not found on the classpath", resourceName));
        }
        return resourceAsStream;
    }
}
