/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.component.workflow.execution.internal;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.rcenvironment.core.component.execution.api.BatchedConsoleRowsProcessor;
import de.rcenvironment.core.component.execution.api.BatchingConsoleRowsForwarder;
import de.rcenvironment.core.component.execution.api.ComponentExecutionException;
import de.rcenvironment.core.component.execution.api.ConsoleRow;
import de.rcenvironment.core.component.execution.api.ConsoleRow.Type;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionException;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.TempFileService;
import de.rcenvironment.core.utils.common.TempFileServiceAccess;

/**
 * Writes {@link ConsoleRow}s into a log files and stores them into the data management.
 * 
 * @author Doreen Seider
 * 
 * Note: The console log file writing was added very late. I'm not happy with the link to the code that needs to trigger the writing
 * and the data management. The data management only accepts console log files during a component run. If there is any log message
 * written outside of one, it simply fails which is pretty bad. So, the underlying flaw might be found in the data model and not
 * that much in this class though. -- seid_do
 */
public class ComponentsConsoleLogFileWriter {
    
    private static final int WAIT_INTERVAL_FLUSHED_AND_DISPOSED = 60;

    private static final String UNDERSCORE = "_";
    
    private static final String ALLOWED_CHARACTERS = "[^a-zA-Z0-9.-]";
    
    private final WorkflowExecutionStorageBridge wfDataManagementStorage;
    
    private AtomicReference<BatchingConsoleRowsForwarder> errorWorkflowConsoleRowWriter = new AtomicReference<>();

    private final Map<String, BatchingConsoleRowsForwarder> completeConsoleRowWriters
        = Collections.synchronizedMap(new HashMap<String, BatchingConsoleRowsForwarder>());
    
    private final Map<String, BatchingConsoleRowsForwarder> errorConsoleRowWriters
        = Collections.synchronizedMap(new HashMap<String, BatchingConsoleRowsForwarder>());

    private AtomicReference<CountDownLatch> logFilesFlushedAndDisposedLatch = new AtomicReference<>();
    
    protected ComponentsConsoleLogFileWriter(WorkflowExecutionStorageBridge wfDataManagementStorage) {
        this.wfDataManagementStorage = wfDataManagementStorage;
    }
    
    protected void initializeWorkflowLogFile() throws IOException {
        errorWorkflowConsoleRowWriter.set(new BatchingConsoleRowsForwarder(new BactchedWorkflowErrorLogFileWriter("")));
        logFilesFlushedAndDisposedLatch.set(new CountDownLatch(1));
    }

    protected void initializeComponentLogFile(String compExeId) throws IOException {
        logFilesFlushedAndDisposedLatch.set(new CountDownLatch((int) logFilesFlushedAndDisposedLatch.get().getCount() + 2));
        completeConsoleRowWriters.put(compExeId, new BatchingConsoleRowsForwarder(new ComponentCompleteLogFileWriter(compExeId)));
        errorConsoleRowWriters.put(compExeId, new BatchingConsoleRowsForwarder(new ComponentErrorLogFileWriter(compExeId)));
    }

    protected void addComponentConsoleRow(ConsoleRow consoleRow) {
        if (completeConsoleRowWriters.containsKey(consoleRow.getComponentIdentifier())) {
            completeConsoleRowWriters.get(consoleRow.getComponentIdentifier()).onConsoleRow(consoleRow);
        } else {
            LogFactory.getLog(getClass()).error(StringUtils.format(
                "Failed to add console row to component's complete log file: %s", consoleRow.getPayload()));
        }
        if (consoleRow.getType() == Type.TOOL_ERROR || consoleRow.getType() == Type.COMPONENT_ERROR
            || consoleRow.getType() == Type.LIFE_CYCLE_EVENT) {
            if (errorConsoleRowWriters.containsKey(consoleRow.getComponentIdentifier())) {
                errorConsoleRowWriters.get(consoleRow.getComponentIdentifier()).onConsoleRow(consoleRow);
            } else {
                LogFactory.getLog(getClass()).error(StringUtils.format(
                    "Failed to add console row to component's error log file: %s", consoleRow.getPayload()));
            }
            errorWorkflowConsoleRowWriter.get().onConsoleRow(consoleRow);
        }
    }
    
    protected void addWorkflowConsoleRow(ConsoleRow consoleRow) {
        errorWorkflowConsoleRowWriter.get().onConsoleRow(consoleRow);
    }
    
    protected void flushAndDisposeLogFiles() {
        //FIXME: Iteration over a view of a synchronized collection without holding a lock on the backing collection. - rode_to, Dec 2016
        for (BatchingConsoleRowsForwarder writer : completeConsoleRowWriters.values()) {
            writer.onConsoleRow(null); // should be improved by dedicated ConsoleRow instance
        }
        //FIXME: Iteration over a view of a synchronized collection without holding a lock on the backing collection. - rode_to, Dec 2016
        for (BatchingConsoleRowsForwarder writer : errorConsoleRowWriters.values()) {
            writer.onConsoleRow(null); // should be improved by dedicated ConsoleRow instance
        }
        errorWorkflowConsoleRowWriter.get().onConsoleRow(null); // should be improved by dedicated ConsoleRow instance
        
        try {
            boolean terminated;
            synchronized (this) {
                terminated = logFilesFlushedAndDisposedLatch.get().await(WAIT_INTERVAL_FLUSHED_AND_DISPOSED, TimeUnit.SECONDS);
            }
            if (!terminated) {
                LogFactory.getLog(getClass()).error("Time out exceeded while waiting for log files to be written");
            }
        } catch (InterruptedException e) {
            LogFactory.getLog(getClass()).error("Failed to wait log files to be written", e);
        }
    }
    
    
    /**
     * Writes the {@link ConsoleRow}s to the complete log file of a component in the RCE temp directory and stores the file in the RCE data
     * management on dedicated {@link ConsoleRow}s.
     * 
     * @author Doreen Seider
     */
    protected class ComponentCompleteLogFileWriter extends AbstractBatchedComponentLogFileWriter {
        
        private static final String VERSION = "1.0";
        
        protected ComponentCompleteLogFileWriter(String exeId) throws IOException {
            super(exeId);
        }

        @Override
        protected void storeLogFileInDataManagement(ConsoleRow triggerConsoleRow) {
            try {
                String[] payload = StringUtils.splitAndUnescape(triggerConsoleRow.getPayload());
                String logFileName = StringUtils.format("%s-%s_compl.log", triggerConsoleRow.getComponentName()
                    .replaceAll(ALLOWED_CHARACTERS, UNDERSCORE), payload[2]);
                wfDataManagementStorage.addComponentCompleteLog(logFile, logFileName, payload[1]);
            } catch (ComponentExecutionException e) {
                logErrorOnWritingToDMFailure(e, triggerConsoleRow);
            }
        }
        
        @Override
        protected String formatConsoleRow(ConsoleRow consoleRow) {
            return consoleRowFormatter.toComponentCompleteLogFileFormat(consoleRow);
        }

        @Override
        protected String getFileFormatVersion() {
            return VERSION;
        }
    }
    
    /**
     * Writes the {@link ConsoleRow}s to the error log file of a component in the RCE temp directory and stores the file in the RCE data
     * management on dedicated {@link ConsoleRow}s.
     * 
     * @author Doreen Seider
     */
    protected class ComponentErrorLogFileWriter extends AbstractBatchedComponentLogFileWriter {
        
        private static final String VERSION = "1.0";
        
        protected ComponentErrorLogFileWriter(String exeId) throws IOException {
            super(exeId);
        }

        @Override
        protected void storeLogFileInDataManagement(ConsoleRow triggerConsoleRow) {
            try {
                String[] payload = StringUtils.splitAndUnescape(triggerConsoleRow.getPayload());
                String logFileName = StringUtils.format("%s-%s_err.log", triggerConsoleRow.getComponentName()
                    .replaceAll(ALLOWED_CHARACTERS, UNDERSCORE), payload[2]);
                wfDataManagementStorage.addComponentErrorLog(logFile, logFileName, payload[1]);
            } catch (ComponentExecutionException e) {
                logErrorOnWritingToDMFailure(e, triggerConsoleRow);
            }
        }
        
        @Override
        protected String formatConsoleRow(ConsoleRow consoleRow) {
            return consoleRowFormatter.toComponentErrorLogFileFormat(consoleRow);
        }
        
        @Override
        protected String getFileFormatVersion() {
            return VERSION;
        }
    }
    
    /**
     * Writes the {@link ConsoleRow}s to the error log file of a workflow in the RCE temp directory and stores the file in the RCE data
     * management on dedicated {@link ConsoleRow}s.
     * 
     * @author Doreen Seider
     */
    protected class BactchedWorkflowErrorLogFileWriter extends AbstractBatchedLogFileWriter {
        
        private static final String VERSION = "1.1";
        
        protected BactchedWorkflowErrorLogFileWriter(String exeId) throws IOException {
            super(exeId);
        }

        @Override
        protected void storeLogFileInDataManagement(ConsoleRow triggerConsoleRow) {
            try {
                String logFileName = StringUtils.format("%s-err.log", triggerConsoleRow.getWorkflowName()
                    .replaceAll(ALLOWED_CHARACTERS, UNDERSCORE));
                wfDataManagementStorage.addWorkflowErrorLog(logFile, logFileName);
            } catch (WorkflowExecutionException e) {
                log.error(StringUtils.format(FAILED_TO_STORE_LOG_FILE_IN_DM, 
                    triggerConsoleRow.getWorkflowName(), triggerConsoleRow.getWorkflowIdentifier(), logFile.getName()), e);
            }
        }

        @Override
        protected boolean isTriggerForWritingLogFileToDM(ConsoleRow consoleRow) {
            return consoleRow.getType() == ConsoleRow.Type.LIFE_CYCLE_EVENT
                && consoleRow.getPayload().startsWith(ConsoleRow.WorkflowLifecyleEventType.WORKFLOW_LOG_FINISHED.name());
        }
        
        @Override
        protected String formatConsoleRow(ConsoleRow consoleRow) {
            return consoleRowFormatter.toWorkflowErrorLogFileFormat(consoleRow);
        }
        
        @Override
        protected String getFileFormatVersion() {
            return VERSION;
        }
    }
    
    /**
     * Writes the {@link ConsoleRow}s to a log file in the RCE temp directory and stores the file in the RCE data management on dedicated
     * {@link ConsoleRow}s.
     * 
     * @author Doreen Seider
     */
    protected abstract class AbstractBatchedComponentLogFileWriter extends AbstractBatchedLogFileWriter {

        protected AbstractBatchedComponentLogFileWriter(String exeId) throws IOException {
            super(exeId);
        }
        
        @Override
        protected boolean isTriggerForWritingLogFileToDM(ConsoleRow consoleRow) {
            return consoleRow.getType() == ConsoleRow.Type.LIFE_CYCLE_EVENT
                && consoleRow.getPayload().startsWith(ConsoleRow.WorkflowLifecyleEventType.COMPONENT_LOG_FINISHED.name());
        }
        
        protected void logErrorOnWritingToDMFailure(ComponentExecutionException e, ConsoleRow consoleRow) {
            log.error(StringUtils.format(FAILED_TO_STORE_LOG_FILE_IN_DM, 
                consoleRow.getWorkflowName(), consoleRow.getWorkflowIdentifier(), logFile.getName()), e);
        }
        
    }
    
    /**
     * Writes the {@link ConsoleRow}s to a log file in the RCE temp directory and stores the file in the RCE data management on dedicated
     * {@link ConsoleRow}s.
     * 
     * @author Doreen Seider
     */
    protected abstract class AbstractBatchedLogFileWriter implements BatchedConsoleRowsProcessor {
        
        protected static final String FAILED_TO_STORE_LOG_FILE_IN_DM = "Failed to store log file in data management"
            + " - workflow '%s' (%s): %s";
        
        private static final String FAILED_TO_CREATE_TEMPORARY_LOG_FILE = "Failed to create temporary log file: ";

        private static final String FAILED_TO_CLEAR_TEMPORARY_LOG_FILE = "Failed to clear temporary log file: ";

        private static final String FAILED_TO_DELETE_TEMPORARY_LOG_FILE = "Failed to delete temporary log file: ";

        protected final Log log = LogFactory.getLog(getClass());

        protected final ConsoleRowFormatter consoleRowFormatter = new ConsoleRowFormatter();

        protected File logFile;
        
        private volatile boolean logFileDisposed = false;

        private final TempFileService tempFileService;
        
        private final String exeId;
        
        protected AbstractBatchedLogFileWriter(String exeId) throws IOException {
            tempFileService = TempFileServiceAccess.getInstance();
            logFile = tempFileService.createTempFileFromPattern("*");
            this.exeId = exeId;
        }

        @Override
        public void processConsoleRows(ConsoleRow[] consoleRows) {

            if (logFileDisposed) {
                log.debug(StringUtils.format("Log file '%s' already disposed; ignored %d incoming console row(s)", logFile.getName(),
                    consoleRows.length));
                return;
            }
            
            List<String> logFileEntries = new ArrayList<>();
            
            for (ConsoleRow consoleRow : consoleRows) {
                if (consoleRow == null) {
                    if (FileUtils.sizeOf(logFile) != 0) {
                        printLogFileNotEmptyError(logFile);
                    }
                    disposeLogFile();
                    return;
                } else if (isTriggerForWritingLogFileToDM(consoleRow)) {
                    writeConsoleRowsToFile(logFileEntries);
                    if (FileUtils.sizeOf(logFile) > 0) {
                        writeVersionToFile();
                        storeLogFileInDataManagement(consoleRow);
                        clearLogFile();
                        logFileEntries.clear();
                    }
                } else if (isMatchingConsoleRow(consoleRow)) {
                    logFileEntries.add(formatConsoleRow(consoleRow));
                }
            }
            writeConsoleRowsToFile(logFileEntries);
        }
        
        protected abstract boolean isTriggerForWritingLogFileToDM(ConsoleRow consoleRow);
        
        protected boolean isMatchingConsoleRow(ConsoleRow consoleRow) {
            return consoleRow.getType() != Type.LIFE_CYCLE_EVENT;
        }
        
        protected abstract String formatConsoleRow(ConsoleRow consoleRow);
        
        protected abstract String getFileFormatVersion();
        
        private void printLogFileNotEmptyError(File file) {
            String fileContent = "[not available]";
            try (FileInputStream inputStream = new FileInputStream(logFile)) {
                fileContent = IOUtils.toString(inputStream);
            } catch (IOException e) {
                log.debug(StringUtils.format("Failed to get content of log file %s; cause: &s", logFile.getAbsolutePath(), e.toString()));
            }
            log.error(StringUtils.format("Request to dispose non-empty log file (related to %s), means that"
                + " the content get lost as it it was not stored to the data management before; file: %s; size: %d, content: %s",
                exeId, file.getAbsolutePath(), FileUtils.sizeOf(file), fileContent));
        }
        
        private void writeConsoleRowsToFile(List<String> logFileEntries) {
            try {
                FileUtils.writeLines(logFile, logFileEntries, true);
            } catch (IOException e) {
                log.error("Failed to add a console log row to the log file: " + logFile, e);
            }
        }

        private void writeVersionToFile() {
            try {
                FileUtils.writeStringToFile(logFile, StringUtils.format("[Log file format version: %s]\n", getFileFormatVersion()), true);
            } catch (IOException e) {
                log.error("Failed to add a console log row to the log file: " + logFile, e);
            }
        }
        
        private void disposeLogFile() {
            try {
                tempFileService.disposeManagedTempDirOrFile(logFile);
            } catch (IOException e) {
                log.error(FAILED_TO_DELETE_TEMPORARY_LOG_FILE + logFile.getAbsolutePath(), e);
            }
            logFileDisposed = true;
            logFilesFlushedAndDisposedLatch.get().countDown();
        }
        
        protected abstract void storeLogFileInDataManagement(ConsoleRow triggerConsoleRow);
        
        private void clearLogFile() {
            try {
                FileUtils.writeStringToFile(logFile, "", false);
            } catch (IOException e) {
                log.error(FAILED_TO_CLEAR_TEMPORARY_LOG_FILE + logFile.getAbsolutePath(), e);
                try {
                    tempFileService.disposeManagedTempDirOrFile(logFile);
                } catch (IOException e1) {
                    log.error(FAILED_TO_DELETE_TEMPORARY_LOG_FILE + logFile.getAbsolutePath(), e);
                } finally {
                    try {
                        logFile = tempFileService.createTempFileFromPattern("*");
                    } catch (IOException e1) {
                        log.error(FAILED_TO_CREATE_TEMPORARY_LOG_FILE + logFile.getAbsolutePath(), e);
                    }
                }
            }
        }
        
    }
}
