/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.context;

import org.apache.log4j.Logger;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

/**
 * BatchContainerDirectory knows the path to the directory for the BatchContainerStep and BatchStepTrigger semaphore files.
 * It also handles much of the logic for writing, reading, and removing those files.
 * <p>
 * BatchContainerDirectory adds a ConsoleAppender to its Logger if one hasn't been configured.
 */
public class BatchContainerDirectory {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BatchContainerDirectory.class);

    private static final String BATCH_CONTAINER_SEMAPHORE_EXTENSION = "runlock";

    private File directory;

    /**
     * Creates a reference for the path to the batch container. Verifies that the directory exists and throws an error if it does not.
     *
     * @param batchContainerPath the path to the directory that contains the semaphore files
     */
    public BatchContainerDirectory(String batchContainerPath) {
        directory = new File(batchContainerPath);

        if (!directory.exists()) {
            throw new RuntimeException(batchContainerPath + " does not exist. Please verify and run again");
        } else {
            LOG.info("batchContainerDirectory=" + batchContainerPath);
        }
    }

    /**
     * Writes the semaphore file which indicates that the batch container is running
     *
     * @param jobName  the name of the job that is starting the batch container
     * @param stepName the name of the step which starts the batch container
     */
    public void writeBatchContainerSemaphore(String jobName, String stepName) {
        if (!isExistsBatchContainerSemaphore()) {
            BatchStepFileDescriptor batchStepFile = new BatchStepFileDescriptor(jobName, stepName, BATCH_CONTAINER_SEMAPHORE_EXTENSION);
            writeBatchStepFileToSystem(batchStepFile, null);
        }
    }

    /**
     * Removes the semaphore file which indicates that the batch container is running
     */
    public void removeBatchContainerSemaphore() {
        FilenameFilter semaphoreExtensionFilter = new FileExtensionFileFilter(BATCH_CONTAINER_SEMAPHORE_EXTENSION);
        File[] filteredFiles = directory.listFiles(semaphoreExtensionFilter);

        for (File semaphore : filteredFiles) {
            semaphore.delete();
        }
    }

    /**
     * Checks whether the batch container is currently running (by checking for the presence of the batch container semaphore file)
     *
     * @return true if the batch container is running (if the semaphore file exists), false otherwise
     */
    public boolean isBatchContainerRunning() {
        return isExistsBatchContainerSemaphore();
    }

    /**
     * @return true if the batch container semaphore file exists in the batch container, false otherwise
     */
    private boolean isExistsBatchContainerSemaphore() {
        FilenameFilter semaphoreExtensionFilter = new FileExtensionFileFilter(BATCH_CONTAINER_SEMAPHORE_EXTENSION);
        File[] filteredFiles = directory.listFiles(semaphoreExtensionFilter);

        if (filteredFiles != null && filteredFiles.length > 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns a list of files in the batch container with the run extension
     *
     * @return a list of step .run files
     */
    public File[] getStepRunFiles() {
        // Find all the step .run files for all the jobs.
        FilenameFilter stepStartFilter = new FileExtensionFileFilter(BatchStepFileDescriptor.getFileExtensionRun());
        File[] stepStartFiles = directory.listFiles(stepStartFilter);

        // No .run files found
        if (stepStartFiles == null || stepStartFiles.length == 0) {
            return null;
        }

        return stepStartFiles;
    }

    /**
     * Writes the run file for the Step specified in the descriptor
     *
     * @param batchStepFile the step for which a .run file will be written
     * @param stepIndex     the index of the step in the job
     */
    public void writeBatchStepRunFile(BatchStepFileDescriptor batchStepFile, int stepIndex) {
        BatchStepFileDescriptor runFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionRun());

        // If successFile already exists then container will think the Step completed before it actually did
        File successFile = new File(getDirectoryPath() + runFile.getNameNoExtension() + BatchStepFileDescriptor.STEP_FILE_EXTENSION_SEPARATOR + BatchStepFileDescriptor.getFileExtensionSuccess());
        if (successFile.exists()) {
            throw new RuntimeException(successFile.getName() + " shouldn't exist yet before Step is run");
        }

        writeBatchStepFileToSystem(runFile, new Integer(stepIndex));
    }

    /**
     * Writes the success file for the Step specified in the descriptor
     *
     * @param batchStepFile the step for which a .success file will be written
     */
    public void writeBatchStepSuccessfulResultFile(BatchStepFileDescriptor batchStepFile) {
        BatchStepFileDescriptor successFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionSuccess());
        writeBatchStepFileToSystem(successFile, null);
    }

    /**
     * Writes the error file for the Step specified in the descriptor
     *
     * @param batchStepFile the step for which a .error file will be written
     */
    public void writeBatchStepErrorResultFile(BatchStepFileDescriptor batchStepFile) {
        BatchStepFileDescriptor errorFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionError());
        writeBatchStepFileToSystem(errorFile, null);
    }


    /**
     * Writes the error file for the Step specified in the descriptor. The stack trace in the Throwable will be written to the file.
     *
     * @param batchStepFile the step for which a .error file will be written
     * @param error         the error to write in the error file
     */
    public void writeBatchStepErrorResultFile(BatchStepFileDescriptor batchStepFile, Throwable error) {
        BatchStepFileDescriptor errorFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionError());
        writeBatchStepFileToSystem(errorFile, error);
    }

    /**
     * Returns a copy of the descriptor passed into the method, but with the extension specified.
     *
     * @param batchStepFile the Step for which a new descriptor is needed
     * @param extension     the extension to use for the new descriptor
     * @return a copy of the descriptor passed in, but with the specified extension
     */
    private BatchStepFileDescriptor getCopyWithNewExtension(BatchStepFileDescriptor batchStepFile, String extension) {
        return new BatchStepFileDescriptor(batchStepFile.getJobName(), batchStepFile.getStepName(), extension);
    }

    /**
     * Writes a batch step semaphore file to the directory; Writes the details to the file.
     * Throws a RuntimeException if the file already exists.
     *
     * @param batchStepFile the step for which a semaphore file will be written
     * @param details       additional details to add to the semaphore file
     */
    private void writeBatchStepFileToSystem(BatchStepFileDescriptor batchStepFile, Object details) {
        LOG.debug("Writing " + batchStepFile.getExtension() + " file for " + batchStepFile);

        String fileName = getDirectoryPath() + batchStepFile.getName();
        File file = new File(fileName);
        if (!file.exists()) {
            try {
                LOG.info("Creating new " + batchStepFile.getExtension() + " file: " + file.getName());
                file.createNewFile();

                //if further details exist write them to the file
                if (details != null) {
                    if (details instanceof Throwable) {
                        writeErrorMessageToFile(file, (Throwable) details);
                    } else if (details instanceof Integer) {
                        writeStepIndexToFile(file, (Integer) details);
                    }

                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new RuntimeException("Step " + batchStepFile.getExtension() + " file: " + fileName + " already exists");
        }
    }

    /**
     * Removes the semaphore file for the specified step
     *
     * @param batchStepFile the step for which the semaphore file should be removed
     */
    public void removeBatchStepFileFromSystem(BatchStepFileDescriptor batchStepFile) {
        LOG.info("Removing " + batchStepFile.getExtension() + " file for " + batchStepFile);

        String fileName = getDirectoryPath() + batchStepFile.getName();
        File file = new File(fileName);
        if (file != null && file.exists()) {
            boolean successfulDelete = file.delete();
            if (!successfulDelete) {
                LOG.error("Failed to delete " + fileName + " from the system.");
            } else if (file.exists()) {
                // Some file systems use caching which can cause odd container behavior particularly
                // if SEMAPHORE_PROCESSING_INTERVAL is set low. This statement could be enhanced to
                // loop until the file disappears
                LOG.warn("Deleted " + fileName + " from the system but file still exists. File system slow or cached?");
            }
        } else {
            //don't need to throw an exception if the file doesn't exist- but note it in the logs
            LOG.warn("Step " + batchStepFile.getExtension() + " file: " + fileName + " doesn't exist");
        }
    }

    /**
     * Returns the result file (either a success file  or an error file) if one exists for the specified step. Otherwise null is returned.
     *
     * @param batchStepFile the step for which a result file is requested
     * @return the descriptor of the result file if one exists, null otherwise
     */
    public BatchStepFileDescriptor getResultFile(BatchStepFileDescriptor batchStepFile) {
        LOG.debug("Looking for a result file for " + batchStepFile);

        String successFileName = getDirectoryPath() + batchStepFile.getNameNoExtension() + "." + BatchStepFileDescriptor.getFileExtensionSuccess();
        File successFile = new File(successFileName);
        if (successFile != null && successFile.exists()) {
            LOG.info("Found .success result file for " + batchStepFile);

            return new BatchStepFileDescriptor(successFile);
        }

        String errorFileName = getDirectoryPath() + batchStepFile.getNameNoExtension() + "." + BatchStepFileDescriptor.getFileExtensionError();
        File errorFile = new File(errorFileName);
        if (errorFile != null && errorFile.exists()) {
            LOG.info("Found .error result file for " + batchStepFile);

            return new BatchStepFileDescriptor(errorFile);
        }


        return null;
    }

    /**
     * Checks whether the step's semaphore file in the descriptor is null and if not whether the step's semaphore file in the directory is empty
     *
     * @param batchStepFile the descriptor for which a semaphore file existence check is requested
     * @return true if the step's semaphore file is empty, false otherwise. Throws a RuntimeException if the file does not exist.
     */
    public boolean isFileEmpty(BatchStepFileDescriptor batchStepFile) {
        File resultFile = batchStepFile.getStepFile();
        if (resultFile == null) {
            throw new RuntimeException(batchStepFile + BatchStepFileDescriptor.STEP_FILE_EXTENSION_SEPARATOR + batchStepFile.getExtension() + " does not exist");
        }

        return isFileEmpty(resultFile);
    }

    /**
     * Returns the index of the step contained in the file
     *
     * @param batchStepFile the file from which to retrieve the step index
     * @return the step index, or -1 if there was no content in the file
     */
    public int getStepIndexFromFile(BatchStepFileDescriptor batchStepFile) {
        File runFile = batchStepFile.getStepFile();
        if (runFile != null) {
            List<String> contents = getFileContents(runFile);
            if (contents.size() > 0) {
                return Integer.parseInt(contents.get(0));
            }
        }

        return -1;
    }

    /**
     * Reads the contents of the semaphore file (normally the error file), and writes the contents to the requested Logger.
     *
     * @param batchStepFile the descriptor whose semaphore file's contents should be written to the Logger
     * @param log           the log to write the file contents to
     */
    public void logFileContents(BatchStepFileDescriptor batchStepFile, Logger log) {
        File resultFile = batchStepFile.getStepFile();
        if (resultFile != null) {
            List<String> contents = getFileContents(resultFile);
            String toLog = "";

            for (String line : contents) {
                toLog += line + "\n";
            }

            log.error("Exception found in " + resultFile.getName() + "\n" + toLog);
        }
    }

    /**
     * Returns the Exception contained in an error result file
     *
     * @param errorResultFile
     * @return the exception
     */
    public String getExceptionFromFile(BatchStepFileDescriptor errorResultFile) {
        if (errorResultFile.isStepFileAnErrorResultFile()) {
            File resultFile = errorResultFile.getStepFile();
            if (resultFile != null) {
                List<String> contents = getFileContents(resultFile);
                String toLog = "";

                for (String line : contents) {
                    toLog += line + "\n";
                }

                return toLog;
            }
        }

        return "";
    }

    /**
     * Creates a shutdown hook and adds it to the local Runtime so that if the container's VM is shut down,
     * the semaphore files will be removed
     */
    public void addShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread() {

            @Override
            public void run() {
                removeBatchContainerSemaphore();
                if (isBatchContainerRunning()) {
                    LOG.info("the batch container was not shut down successfully; .runlock still exists.");
                }
            }

        });
    }

    /**
     * Returns a List<String> containing each line in the file
     *
     * @param file the file whose contents we want to retrieve
     * @return a String array containing each line in the file
     */
    private List<String> getFileContents(File file) {
        ArrayList<String> results = new ArrayList<String>();
        try {
            FileReader fileReader = new FileReader(file.getAbsolutePath());

            String line = "";
            BufferedReader inputBufferedReader = new BufferedReader(fileReader);

            String currentLine = inputBufferedReader.readLine();
            while (currentLine != null) {
                results.add(currentLine);
                currentLine = inputBufferedReader.readLine();
            }
            inputBufferedReader.close();

            return results;

        } catch (IOException e) {
            throw new RuntimeException("getFileContents() : " + e.getMessage(), e);
        }
    }

    /**
     * Returns the absolute path to the directory
     *
     * @return the absolute path to the batch container directory
     */
    private String getDirectoryPath() {
        return directory.getAbsolutePath() + File.separator;
    }

    /**
     * Checks whether the specified File is empty.
     *
     * @param file the file to check
     * @return true if the file is empty, false otherwise
     */
    private boolean isFileEmpty(File file) {
        return file.length() == 0;
    }


    private void writeMessageToFile(File runFile, String message) {
        PrintStream printStream = initializePrintStream(runFile);

        //write index
        printStream.print(message);
        printStream.flush();

        destroyPrintStream(printStream);
    }

    /**
     * Writes the index of the step to the File
     *
     * @param runFile   the file to write to
     * @param stepIndex the step index to write
     */
    private void writeStepIndexToFile(File runFile, Integer stepIndex) {
        PrintStream printStream = initializePrintStream(runFile);

        //write index
        printStream.print(stepIndex.intValue());
        printStream.flush();

        destroyPrintStream(printStream);
    }

    /**
     * Writes the stack trace of Throwable to the File
     *
     * @param errorFile the file to write to
     * @param error     the error to write
     */
    private void writeErrorMessageToFile(File errorFile, Throwable error) {
        PrintStream printStream = initializePrintStream(errorFile);

        //write error
        error.printStackTrace(printStream);

        //write contents of the batch container directory
        printStream.println("BatchContainerDirectory contents:");
        File[] batchContainerFiles = directory.listFiles();

        if (batchContainerFiles == null || batchContainerFiles.length == 0) {
            printStream.println("empty");
        } else {
            for (File batchContainerFile : batchContainerFiles) {
                printStream.println(batchContainerFile.getAbsolutePath());
            }
        }

        printStream.flush();

        destroyPrintStream(printStream);
    }

    /**
     * Initializes a PrintStream to use for writing to.
     *
     * @param errorFile the file to write to
     * @return a new PrintStream to use to write to the file
     */
    private PrintStream initializePrintStream(File errorFile) {
        //initialize PrintStream
        PrintStream printStream;
        try {
            printStream = new PrintStream(errorFile);
        } catch (FileNotFoundException e) {
            LOG.error(e);
            throw new RuntimeException(e);
        }

        return printStream;
    }

    /**
     * Closes the PrintStream
     *
     * @param printStream the print stream to close
     */
    private void destroyPrintStream(PrintStream printStream) {
        //close PrintStream
        if (printStream != null) {
            printStream.close();
            printStream = null;
        }
    }
}
