/*
 * Copyright 2012 International Business Machines Corp.
 * 
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership. Licensed under the Apache License, 
 * Version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.batchee.container.impl.controller;

import org.apache.batchee.container.ExecutionElementController;
import org.apache.batchee.container.exception.BatchContainerRuntimeException;
import org.apache.batchee.container.exception.BatchContainerServiceException;
import org.apache.batchee.container.impl.MetricImpl;
import org.apache.batchee.container.impl.StepContextImpl;
import org.apache.batchee.container.impl.StepExecutionImpl;
import org.apache.batchee.container.impl.controller.chunk.PersistentDataWrapper;
import org.apache.batchee.container.impl.jobinstance.RuntimeJobExecution;
import org.apache.batchee.container.services.BatchKernelService;
import org.apache.batchee.container.services.JobStatusManagerService;
import org.apache.batchee.container.services.ServicesManager;
import org.apache.batchee.container.status.ExecutionStatus;
import org.apache.batchee.container.status.ExtendedBatchStatus;
import org.apache.batchee.container.status.StepStatus;
import org.apache.batchee.container.util.PartitionDataWrapper;
import org.apache.batchee.jaxb.JSLProperties;
import org.apache.batchee.jaxb.Property;
import org.apache.batchee.jaxb.Step;
import org.apache.batchee.spi.PersistenceManagerService;
import org.apache.batchee.spi.TransactionManagementService;
import org.apache.batchee.spi.TransactionManagerAdapter;

import jakarta.batch.operations.JobExecutionAlreadyCompleteException;
import jakarta.batch.operations.JobExecutionNotMostRecentException;
import jakarta.batch.operations.JobRestartException;
import jakarta.batch.operations.JobStartException;
import jakarta.batch.runtime.BatchStatus;
import jakarta.batch.runtime.JobInstance;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Change the name of this class to something else!! Or change BaseStepController.
 */
public abstract class BaseStepController implements ExecutionElementController {
    private static final Logger LOGGER = Logger.getLogger(BaseStepController.class.getName());

    protected RuntimeJobExecution jobExecutionImpl;
    protected JobInstance jobInstance;

    protected StepContextImpl stepContext;
    protected Step step;
    protected String stepName;
    protected StepStatus stepStatus;

    protected BlockingQueue<PartitionDataWrapper> analyzerStatusQueue = null;

    protected long rootJobExecutionId;

    // Restart of partitioned steps needs to be handled specially
    protected boolean restartAfterCompletion = false;

    protected final BatchKernelService kernelService;
    protected final PersistenceManagerService persistenceManagerService;
    private final JobStatusManagerService statusManagerService;

    protected TransactionManagerAdapter transactionManager = null;
    private TransactionManagementService txService;

    protected BaseStepController(final RuntimeJobExecution jobExecution, final Step step, final StepContextImpl stepContext, final long rootJobExecutionId,
                                 final ServicesManager servicesManager) {
        this.jobExecutionImpl = jobExecution;
        this.jobInstance = jobExecution.getJobInstance();
        this.stepContext = stepContext;
        this.rootJobExecutionId = rootJobExecutionId;
        if (step == null) {
            throw new IllegalArgumentException("Step parameter to ctor cannot be null.");
        }
        this.step = step;
        this.stepName = step.getId();

        this.txService = servicesManager.service(TransactionManagementService.class);
        this.kernelService = servicesManager.service(BatchKernelService.class);
        this.persistenceManagerService = servicesManager.service(PersistenceManagerService.class);
        this.statusManagerService = servicesManager.service(JobStatusManagerService.class);
    }

    protected BaseStepController(final RuntimeJobExecution jobExecution,
                                 final Step step, final StepContextImpl stepContext,
                                 final long rootJobExecutionId,
                                 final BlockingQueue<PartitionDataWrapper> analyzerStatusQueue,
                                 final ServicesManager servicesManager) {
        this(jobExecution, step, stepContext, rootJobExecutionId, servicesManager);
        this.analyzerStatusQueue = analyzerStatusQueue;
    }

    ///////////////////////////
    // ABSTRACT METHODS ARE HERE
    ///////////////////////////
    protected abstract void invokeCoreStep() throws JobRestartException, JobStartException, JobExecutionAlreadyCompleteException, JobExecutionNotMostRecentException;

    protected abstract void setupStepArtifacts();

    protected abstract void invokePreStepArtifacts();

    protected abstract void invokePostStepArtifacts();

    // This is only useful from the partition threads
    protected abstract void sendStatusFromPartitionToAnalyzerIfPresent();

    @Override
    public ExecutionStatus execute() {
        // Here we're just setting up to decide if we're going to run the step or not (if it's already complete and
        // allow-start-if-complete=false.
        try {
            boolean executeStep = shouldStepBeExecuted();
            if (!executeStep) {
                return new ExecutionStatus(ExtendedBatchStatus.DO_NOT_RUN, stepStatus.getExitStatus());
            }
        } catch (final Throwable t) {
            // Treat an error at this point as unrecoverable, so fail job too.
            markJobAndStepFailed();
            LOGGER.log(Level.SEVERE, t.getMessage(), t);
            rethrowWithMsg("Caught throwable while determining if step should be executed.  Failing job.", t);
        }

        // At this point we have a StepExecution.  Setup so that we're ready to invoke artifacts.
        try {
            startStep();
        } catch (final Throwable t) {
            // Treat an error at this point as unrecoverable, so fail job too.
            markJobAndStepFailed();
            LOGGER.log(Level.SEVERE, t.getMessage(), t);
            rethrowWithMsg("Caught throwable while starting step.  Failing job.", t);
        }

        // At this point artifacts are in the picture so we want to try to invoke afterStep() on a failure.
        try {
            invokePreStepArtifacts();    //Call PartitionReducer and StepListener(s)
            invokeCoreStep();
        } catch (final Exception e) {
            // We're going to continue on so that we can execute the afterStep() and analyzer
            try {
                LOGGER.log(Level.SEVERE, e.getMessage(), e);
                markStepFailed();
            } catch (final Throwable t) {
                // Since the first one is the original first failure, let's rethrow t1 and not the second error,
                // but we'll log a severe error pointing out that the failure didn't get persisted..
                // We won't try to call the afterStep() in this case either.
                rethrowWithMsg("ERROR. PERSISTING BATCH STATUS FAILED.  STEP EXECUTION STATUS TABLES MIGHT HAVE CONSISTENCY ISSUES" +
                    "AND/OR UNEXPECTED ENTRIES.", t);
            }
        } catch (final Throwable t) {
            LOGGER.log(Level.SEVERE, t.getMessage(), t);
            markJobAndStepFailed();
        }

        //
        // At this point we may have already failed the step, but we still try to invoke the end of step artifacts.
        //
        try {
            //Call PartitionAnalyzer, PartitionReducer and StepListener(s)
            invokePostStepArtifacts();
        } catch (final Throwable t) {
            LOGGER.log(Level.SEVERE, t.getMessage(), t);
            markStepFailed();
        }

        //
        // No more application code is on the path from here on out (excluding the call to the PartitionAnalyzer
        // analyzeStatus().  If an exception bubbles up and leaves the statuses inconsistent or incorrect then so be it;
        // maybe there's a runtime bug that will need to be fixed.
        //
        try {
            // Now that all step-level artifacts have had a chance to run,
            // we set the exit status to one of the defaults if it is still unset.

            // This is going to be the very last sequence of calls from the step running on the main thread,
            // since the call back to the partition analyzer only happens on the partition threads.
            // On the partition threads, then, we harden the status at the partition level before we
            // send it back to the main thread.
            persistUserData();
            transitionToFinalBatchStatus();
            defaultExitStatusIfNecessary();
            persistExitStatusAndEndTimestamp();
        } catch (final Throwable t) {
            // Don't let an exception caught here prevent us from persisting the failed batch status.
            markJobAndStepFailed();
            rethrowWithMsg("Failure ending step execution", t);
        }

        //
        // Only happens on main thread.
        //
        sendStatusFromPartitionToAnalyzerIfPresent();

        if (stepStatus.getBatchStatus().equals(BatchStatus.FAILED)) {
            return new ExecutionStatus(ExtendedBatchStatus.EXCEPTION_THROWN, stepStatus.getExitStatus());
        }
        return new ExecutionStatus(ExtendedBatchStatus.NORMAL_COMPLETION, stepStatus.getExitStatus());
    }

    private void defaultExitStatusIfNecessary() {
        final String stepExitStatus = stepContext.getExitStatus();
        final String processRetVal = stepContext.getBatchletProcessRetVal();
        if (stepExitStatus == null) {
            if (processRetVal != null) {
                stepContext.setExitStatus(processRetVal);
            } else {
                stepContext.setExitStatus(stepContext.getBatchStatus().name());
            }
        }
    }

    private void markStepFailed() {
        updateBatchStatus(BatchStatus.FAILED);
    }

    protected void markJobAndStepFailed() {
        jobExecutionImpl.getJobContext().setBatchStatus(BatchStatus.FAILED);
        markStepFailed();
    }

    private void startStep() {
        // Update status
        statusStarting();
        //Set Step context properties
        setContextProperties();
        //Set up step artifacts like step listeners, partition reducers
        setupStepArtifacts();
        // Move batch status to started.
        updateBatchStatus(BatchStatus.STARTED);

        long time = System.currentTimeMillis();
        Timestamp startTS = new Timestamp(time);
        stepContext.setStartTime(startTS);

        persistenceManagerService.updateStepExecution(rootJobExecutionId, stepContext);
    }


    /**
     * The only valid states at this point are STARTED,STOPPING, or FAILED.
     * been able to get to STOPPED, or COMPLETED yet at this point in the code.
     */
    private void transitionToFinalBatchStatus() {
        BatchStatus currentBatchStatus = stepContext.getBatchStatus();
        if (currentBatchStatus.equals(BatchStatus.STARTED)) {
            updateBatchStatus(BatchStatus.COMPLETED);
        } else if (currentBatchStatus.equals(BatchStatus.STOPPING)) {
            updateBatchStatus(BatchStatus.STOPPED);
        } else if (currentBatchStatus.equals(BatchStatus.FAILED)) {
            updateBatchStatus(BatchStatus.FAILED);           // Should have already been done but maybe better for possible code refactoring to have it here.
        } else {
            throw new IllegalStateException("Step batch status should not be in a " + currentBatchStatus.name() + " state");
        }
    }

    protected void updateBatchStatus(final BatchStatus updatedBatchStatus) {
        stepStatus.setBatchStatus(updatedBatchStatus);
        statusManagerService.updateStepStatus(stepStatus.getStepExecutionId(), stepStatus);
        stepContext.setBatchStatus(updatedBatchStatus);
    }

    protected boolean shouldStepBeExecuted() {
        this.stepStatus = statusManagerService.getStepStatus(jobInstance.getInstanceId(), step.getId());
        if (stepStatus == null) {
            // create new step execution
            final StepExecutionImpl stepExecution = getNewStepExecution(rootJobExecutionId, stepContext);
            // create new step status for this run
            stepStatus = statusManagerService.createStepStatus(stepExecution.getStepExecutionId());
            stepContext.setInternalStepExecutionId(stepExecution.getStepExecutionId());
            stepContext.setStepExecutionId(stepExecution.getStepExecutionId());
            return true;
        } else {
            // if a step status already exists for this instance id. It means this
            // is a restart and we need to get the previously persisted data
            stepContext.setPersistentUserData(stepStatus.getPersistentUserData());
            if (shouldStepBeExecutedOnRestart()) {
                // Seems better to let the start count get incremented without getting a step execution than
                // vice versa (in an unexpected error case).
                stepStatus.incrementStartCount();
                // create new step execution
                final StepExecutionImpl stepExecution = getNewStepExecution(rootJobExecutionId, stepContext);
                final long stepExecutionId = stepExecution.getStepExecutionId();
                this.stepStatus.setLastRunStepExecutionId(stepExecutionId);
                stepContext.setStepExecutionId(stepExecutionId);
                stepContext.setInternalStepExecutionId(stepExecutionId);
                return true;
            }
            return false;
        }
    }

    private boolean shouldStepBeExecutedOnRestart() {
        BatchStatus stepBatchStatus = stepStatus.getBatchStatus();
        if (stepBatchStatus.equals(BatchStatus.COMPLETED)) {
            // A bit of parsing involved since the model gives us a String not a
            // boolean, but it should default to 'false', which is the spec'd default.
            if (!Boolean.parseBoolean(step.getAllowStartIfComplete())) {
                return false;
            } else {
                restartAfterCompletion = true;
            }
        }

        // The spec default is '0', which we get by initializing to '0' in the next line
        int startLimit = 0;
        String startLimitString = step.getStartLimit();
        if (startLimitString != null) {
            try {
                startLimit = Integer.parseInt(startLimitString);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Could not parse start limit value.  Received NumberFormatException for start-limit value:  " + startLimitString
                    + " for stepId: " + step.getId() + ", with start-limit=" + step.getStartLimit());
            }
        }

        if (startLimit < 0) {
            throw new IllegalArgumentException("Found negative start-limit of " + startLimit + "for stepId: " + step.getId());
        }

        if (startLimit > 0) {
            final int newStepStartCount = stepStatus.getStartCount() + 1;
            if (newStepStartCount > startLimit) {
                throw new IllegalStateException("For stepId: " + step.getId() + ", tried to start step for the " + newStepStartCount
                    + " time, but startLimit = " + startLimit);
            }
        }
        return true;
    }

    protected boolean isRestartExecution() {
        return stepStatus.getStartCount() > 1;
    }

    protected void statusStarting() {
        stepStatus.setBatchStatus(BatchStatus.STARTING);
        statusManagerService.updateJobCurrentStep(jobInstance.getInstanceId(), step.getId());
        statusManagerService.updateStepStatus(stepStatus.getStepExecutionId(), stepStatus);
        stepContext.setBatchStatus(BatchStatus.STARTING);
    }

    protected void persistUserData() {
        PersistentDataWrapper userData = resolveUserData();
        storeUserData(userData);
    }

    protected PersistentDataWrapper resolveUserData() {
        final ByteArrayOutputStream persistentBAOS = new ByteArrayOutputStream();
        final ObjectOutputStream persistentDataOOS;

        try {
            persistentDataOOS = new ObjectOutputStream(persistentBAOS);
            persistentDataOOS.writeObject(stepContext.getPersistentUserData());
            persistentDataOOS.close();
        } catch (final Exception e) {
            throw new BatchContainerServiceException("Cannot persist the persistent user data for the step.", e);
        }

        return new PersistentDataWrapper(persistentBAOS.toByteArray());
    }

    protected void storeUserData(PersistentDataWrapper userData) {
        try {
            stepStatus.setPersistentUserData(userData);
            statusManagerService.updateStepStatus(stepStatus.getStepExecutionId(), stepStatus);
        } catch (final Exception e) {
            throw new BatchContainerServiceException("Cannot persist the persistent user data for the step.", e);
        }
    }

    protected void persistExitStatusAndEndTimestamp() {
        stepStatus.setExitStatus(stepContext.getExitStatus());
        statusManagerService.updateStepStatus(stepStatus.getStepExecutionId(), stepStatus);

        // set the end time metric before flushing
        long time = System.currentTimeMillis();
        Timestamp endTS = new Timestamp(time);
        stepContext.setEndTime(endTS);

        persistenceManagerService.updateStepExecution(rootJobExecutionId, stepContext);
    }

    private StepExecutionImpl getNewStepExecution(long rootJobExecutionId, StepContextImpl stepContext) {
        return persistenceManagerService.createStepExecution(rootJobExecutionId, stepContext);
    }

    private void setContextProperties() {
        JSLProperties jslProps = step.getProperties();

        if (jslProps != null) {
            for (Property property : jslProps.getPropertyList()) {
                Properties contextProps = stepContext.getProperties();
                contextProps.setProperty(property.getName(), property.getValue());
            }
        }

        // set up metrics
        stepContext.addMetric(MetricImpl.MetricType.READ_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.WRITE_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.READ_SKIP_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.PROCESS_SKIP_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.WRITE_SKIP_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.FILTER_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.COMMIT_COUNT, 0);
        stepContext.addMetric(MetricImpl.MetricType.ROLLBACK_COUNT, 0);

        transactionManager = txService.getTransactionManager(stepContext);
    }

    public void setStepContext(final StepContextImpl stepContext) {
        this.stepContext = stepContext;
    }

    @Override
    public List<Long> getLastRunStepExecutions() {
        final List<Long> stepExecIdList = new ArrayList<Long>(1);
        stepExecIdList.add(this.stepStatus.getLastRunStepExecutionId());
        return stepExecIdList;
    }

    private void rethrowWithMsg(final String msgBeginning, final Throwable t) {
        final String errorMsg = msgBeginning + " ; Caught exception/error: " + t.getLocalizedMessage();
        throw new BatchContainerRuntimeException(errorMsg, t);
    }

    public String toString() {
        return "BaseStepController for step = " + step.getId();
    }

    public void setParentStepContext(final StepContextImpl parentStepContext) {
        if (parentStepContext != null) {
            stepContext.setStepExecutionId(parentStepContext.getStepExecutionId());
        }
    }
}
