/*
 * 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
 */
package com.scytl.products.ov.sdm.plugin;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Service
public class SequentialExecutorImpl implements SequentialExecutor {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(SequentialExecutorImpl.class);
    
    public void execute(List<String> commands, Parameters parameters, ExecutionListener listener) {

        for(String command : commands) {
            String mockCommand = command;
            try {
                //Replace the parameters and execute the command
                String[] partialCommands = replaceParameters(command, parameters);
                String fullCommand = partialCommands[0];
                mockCommand = partialCommands[1];
                Process proc = Runtime.getRuntime().exec(fullCommand);

                InputStream isIn = proc.getInputStream();
                consumeProcessStream(isIn, listener);

                StringBuilder stringBuilder = new StringBuilder();
                InputStream isError = proc.getErrorStream();
                consumeProcessStream(isError, stringBuilder);

                final int exitValue = proc.waitFor();
                detectError(exitValue, stringBuilder, mockCommand, listener);
                	
            } catch (IllegalArgumentException e) {
                listener.onError(ResultCode.UNEXPECTED_ERROR.value());
                listener.onMessage(e.getMessage());
                LOGGER.error("Error '"+Integer.toString(ResultCode.UNEXPECTED_ERROR.value())+"' when executing command: "
                    + mockCommand + " : " + e);
            } catch (RuntimeException | IOException e) {
                listener.onError(ResultCode.GENERAL_ERROR.value());
                listener.onMessage(e.getMessage());
                LOGGER.error("Error '"+Integer.toString(ResultCode.GENERAL_ERROR.value())+"' when executing command: "
                    + mockCommand + " : " + e);
            } catch (InterruptedException e) {
                LOGGER.warn("Got interrupted when executing command: " + mockCommand);
                Thread.currentThread().interrupt();
            }           
        }
    }

    private void consumeProcessStream(final InputStream is, final StringBuilder sb) throws IOException {
        String error = IOUtils.toString(is, StandardCharsets.UTF_8);
        sb.append(error);
    }

    private void consumeProcessStream(final InputStream is, final ExecutionListener listener) {
        try
        {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String line;
            LOGGER.debug("Plugin Output ----->>");
            while ((line = br.readLine()) != null) {
                //very crude way of trying do detect error from process output stream.
                if(line.contains("ERROR")) {
                    LOGGER.warn("Plugin execution may have generated an error --->> {}", line);
                    listener.onProgress(line);
                    listener.onError(-1);
                } else {
                    LOGGER.debug(line);
                    listener.onProgress(line);
                }
            }
            LOGGER.debug("<<----- Plugin Output");
        } catch (IOException e) {
            LOGGER.warn("Failed to read plugin execution output", e);
        }
    }

    private String[] replaceParameters(String command, Parameters parameters) {
        String partialCommand = command;
        String replacedCommand = command;
        
        for (KeyParameter key : KeyParameter.values()) {
            if(replacedCommand.contains(key.toString())) {
                String value = parameters.getParam(key.name());
                if(value == null || value.isEmpty()){
                    throw new IllegalArgumentException("Parameter #" + key.name() + "# is null or empty");
                } else {
                	replacedCommand = replacedCommand.replaceAll("#" + key + "#", value);
                    if (key == KeyParameter.PRIVATE_KEY) {
                        partialCommand = partialCommand.replaceAll("#" + key + "#", "PRIVATE_KEY");
                    } else {
                        partialCommand = partialCommand.replaceAll("#" + key + "#", value);
                    }
                }
            }
        }

        return new String[]{ replacedCommand, partialCommand};
    }
    
    /**
     *  Did it fail? unfortunately in our plugin the error code is always '0' even if it internally fails
     *  (unless it throws uncaught exception) which means we don't really get the real exit value
     *  nevertheless, when we consume the process outputstream we try do detect an error and return it in the
     * 'listener' object
     * 
     * @param exitValue
     * @param stringBuilder
     * @param mockCommand
     * @param listener
     */
    private void detectError(int exitValue, StringBuilder stringBuilder, String mockCommand,
			ExecutionListener listener) {
	 
    	String errorOutput = stringBuilder.toString().trim();
    	 
         if(exitValue != 0 || errorOutput.length() > 0) {
             if(exitValue != 0){
                 listener.onError(exitValue);
             } else {
                 try {
                     Integer actualEror = Integer.valueOf(errorOutput);
                     listener.onError(actualEror);
                 } catch (NumberFormatException e){
                     listener.onError(ResultCode.GENERAL_ERROR.value());
                 }
             }
             listener.onMessage("Failed to execute: " + mockCommand);
             LOGGER.error("ExitCode '"+Integer.toString(exitValue)+"' when executing command "
                 + mockCommand + " : " + errorOutput);
         }
    }
}
