/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */
 
package de.rcenvironment.core.utils.cluster.internal;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;

import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

import de.rcenvironment.core.utils.cluster.ClusterJobInformation;
import de.rcenvironment.core.utils.cluster.ClusterJobInformation.ClusterJobState;
import de.rcenvironment.core.utils.cluster.ClusterJobStateChangeListener;
import de.rcenvironment.core.utils.cluster.ClusterService;
import de.rcenvironment.core.utils.ssh.jsch.JschSessionFactory;
import de.rcenvironment.core.utils.ssh.jsch.SshParameterException;
import de.rcenvironment.core.utils.ssh.jsch.SshSessionConfiguration;
import de.rcenvironment.core.utils.ssh.jsch.executor.JSchCommandLineExecutor;

/**
 * Abstract implementation of {@link ClusterService} with common functionality.
 * 
 * @author Doreen Seider
 */
public abstract class AbstractClusterService implements ClusterService {

    protected static final String REMOTE_WORK_DIR = "~";

    protected SshSessionConfiguration sshConfiguration;
    
    protected Map<String, String> pathsToQueuingSystemCommands;

    protected Session jschSession;
    
    protected volatile long latestFetch = 0;
    
    protected Map<String, ClusterJobInformation> latestFetchedJobInformation;
    
    protected Map<String, ClusterJobState> lastClusterJobStates = new HashMap<String, ClusterJobInformation.ClusterJobState>();
    
    protected final Map<String, ClusterJobStateChangeListener> listeners = new HashMap<String, ClusterJobStateChangeListener>();

    protected Timer fetchInformationTimer;
    
    public AbstractClusterService() {}
    
    public AbstractClusterService(SshSessionConfiguration sshConfiguration, Map<String, String> pathsToQueuingSystemCommands) {
        this.sshConfiguration = sshConfiguration;
        this.pathsToQueuingSystemCommands = pathsToQueuingSystemCommands;
    }
    
    public void setPathsToQueuingSystemCommands(Map<String, String> pathsToQueuingSystemCommands) {
        this.pathsToQueuingSystemCommands = pathsToQueuingSystemCommands;
    }
    
    protected void ensureJschSessionEstablished() throws IOException {
        synchronized (this) {
            if (jschSession == null || !jschSession.isConnected()) {
                try {
                    jschSession = JschSessionFactory.setupSession(sshConfiguration.getDestinationHost(),
                        sshConfiguration.getPort(), sshConfiguration.getSshAuthUser(), null,
                        sshConfiguration.getSshAuthPhrase(), null);
                } catch (JSchException e) {
                    throw new IOException("Establishing connection to cluster failed: " + ExceptionUtils.getRootCauseMessage(e), e);
                } catch (SshParameterException e) {
                    throw new IOException("Establishing connection to cluster failed" + ExceptionUtils.getRootCauseMessage(e), e);
                }
            }   
        }
    }
    
    protected String buildMainCommand(String command) {
        // with Java 8 this can be improved by Map.getOrDefault()
        if (pathsToQueuingSystemCommands.get(command) != null) {
            command = pathsToQueuingSystemCommands.get(command) + command;
        }
        return command;
    }
    
    @Override
    public void addClusterJobStateChangeListener(String jobId, final ClusterJobStateChangeListener listener) {
        synchronized (listeners) {
            listeners.put(jobId, listener);
            if (fetchInformationTimer == null) {
                fetchInformationTimer = new Timer("Fetch Cluster Job Information Timer", true);
                TimerTask fetchInformationTimerTask = new TimerTask() {
                    
                    @Override
                    public void run() {
                        final int oneSecond = 1000;
                        if (latestFetchedJobInformation == null || new Date().getTime() - latestFetch > FETCH_INTERVAL + oneSecond) {
                            try {
                                fetchClusterJobInformation();
                            } catch (IOException e) {
                                fetchInformationTimer.cancel();
                                fetchInformationTimer = null;
                                jschSession = null;
                                notifyClusterJobStateChangeListenerAboutFetchingFailure();
                                throw new RuntimeException("Fetching cluster job information failed", e);
                            }
                        }
                        notifyClusterJobStateChangeListener();
                    }
                };
                fetchInformationTimer.schedule(fetchInformationTimerTask, ClusterService.FETCH_INTERVAL,
                    ClusterService.FETCH_INTERVAL);                
            }
        }
    }
    
    private void notifyClusterJobStateChangeListener() {

        Set<String> listenersToRemove = new HashSet<String>();
        
        synchronized (listeners) {
            for (String jobId : listeners.keySet()) {
                ClusterJobInformation.ClusterJobState lastState = lastClusterJobStates.get(jobId);
                if (latestFetchedJobInformation.containsKey(jobId)) {
                    ClusterJobInformation.ClusterJobState latestState = latestFetchedJobInformation.get(jobId).getJobState();
                    if (lastState == null || !lastState.equals(latestState)) {
                        if (!listeners.get(jobId).onClusterJobStateChanged(latestState)) {
                            listenersToRemove.add(jobId);
                        }
                    }
                    lastState = latestState;
                } else {
                    if (!listeners.get(jobId).onClusterJobStateChanged(ClusterJobInformation.ClusterJobState.Unknown)) {
                        listenersToRemove.add(jobId);
                    }
                }
            }
            
            for (String jobId : listenersToRemove) {
                listeners.remove(jobId);
            }
            
            if (listeners.isEmpty()) {
                fetchInformationTimer.cancel();
                fetchInformationTimer = null;
            }
        }
    }
    
    private void notifyClusterJobStateChangeListenerAboutFetchingFailure() {
        
        synchronized (listeners) {
            for (ClusterJobStateChangeListener listener : listeners.values()) {
                listener.onClusterJobStateChanged((ClusterJobState) null);
            }
        }
    }
    
    protected String executesCommand(Session ajschSession, String command, String remoteWorkDir)
        throws IOException {
        JSchCommandLineExecutor commandLineExecutor = new JSchCommandLineExecutor(ajschSession, remoteWorkDir);
        commandLineExecutor.start(command);
        try (InputStream stdoutStream = commandLineExecutor.getStdout(); InputStream stderrStream = commandLineExecutor.getStderr();) {
            try {
                commandLineExecutor.waitForTermination();
            } catch (InterruptedException e) {
                throw new IOException(e);
            }
            String stderr = IOUtils.toString(stderrStream);
            if (stderr != null && !stderr.isEmpty()) {
                throw new IOException(stderr);
            }
            String stdout = IOUtils.toString(stdoutStream);
            return stdout;
        }
    }

}
