package be.iminds.ilabt.jfed.ui.commandline.probe;

import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceId;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceIdParser;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import be.iminds.ilabt.jfed.rspec.parser.extensions.UserSpecParser;
import be.iminds.ilabt.jfed.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * CommandLineClient makes command line client helper methods from a class
 */
public class CommandLineCore<T> {
    private static final Logger LOG = LoggerFactory.getLogger(CommandLineCore.class);

    private final Class<T> targetClass;
    private final T targetObject;

    private final Map<String, Method> availableMethods;
    private final Map<String, Method> availableMethodsLowerCase;

    private static Map<String, Method> findAvailableMethods(Class targetClass) {
        Map<String, Method> res = new HashMap<>();

        for (Method m : targetClass.getDeclaredMethods()) {//GeniConnection con,
            if (m.isAnnotationPresent(ApiMethod.class)) {
                ApiMethod am = m.getAnnotation(ApiMethod.class);
                Class[] parameterTypes = m.getParameterTypes();
                if (parameterTypes.length == 0 || !JFedConnection.class.isAssignableFrom(parameterTypes[0]))
                    throw new RuntimeException("Method " + m + " does not have GeniConnection as first parameter but it does have @ApiMethod");
                res.put(m.getName(), m);
            }
        }

        return res;
    }

    public CommandLineCore(Class<T> targetClass, T targetObject) {
        this.targetClass = targetClass;
        this.targetObject = targetObject;
        availableMethods = findAvailableMethods(targetClass);
        availableMethodsLowerCase = new HashMap<>();
        for (Map.Entry<String, Method> e : availableMethods.entrySet())
            availableMethodsLowerCase.put(e.getKey().toLowerCase(), e.getValue());
    }

    public Collection<String> getAvailableMethodNames() {
        return availableMethods.keySet();
    }

    public Collection<String> getAvailableMethodNamesLowerCase() {
        return availableMethodsLowerCase.keySet();
    }

    /**
     * @param methodName the name of the method
     * @return a String describing the syntax of the method.
     */
    public String getMethodSyntax(String methodName) {
        Method m = availableMethodsLowerCase.get(methodName.toLowerCase());
        Class[] parameterTypes = m.getParameterTypes();
        Annotation[][] annotations = m.getParameterAnnotations();

        String res = "";

        for (int i = 0; i < parameterTypes.length; i++) {
            Annotation[] paramAnnotations = annotations[i];
            Class paramClass = parameterTypes[i];
            for (Annotation paramAnnotation : paramAnnotations)
                if (Objects.equals(paramAnnotation.annotationType(), ApiMethodParameter.class)) {
                    ApiMethodParameter amp = (ApiMethodParameter) paramAnnotation;

                    if (res.length() > 0) res += " ";
                    if (Objects.equals(amp.name(), "credentialList")) {
                        res += "<credential file>";
                    } else {
                        if (Objects.equals(amp.name(), "rspec")) {
                            res += "<rspec file>";
                        } else {
                            if (amp.required()) res += "<";
                            else res += "[";
                            res += amp.name() + "";
                            if (Objects.equals(paramClass, Boolean.class)) res += " (true|false)";
                            if (Objects.equals(paramClass, Integer.class)) res += " (int)";
                            if (amp.required()) res += ">";
                            else res += "]";
                        }
                    }
                }
        }

        return res;
    }

    public static class ExecuteMethodResult {
        int exitvalue = 0;
        String output = "";
    }

    public ExecuteMethodResult executeMethod(JFedConnection connection, String methodName, List<String> arguments, boolean interactive) {
        Method m = availableMethodsLowerCase.get(methodName.toLowerCase());
        Class[] parameterTypes = m.getParameterTypes();
        Annotation[][] annotations = m.getParameterAnnotations();
        ExecuteMethodResult res = new ExecuteMethodResult();

        Object[] parameters = new Object[parameterTypes.length];

        LinkedList<String> remainingArguments = new LinkedList<>(arguments);
        for (int i = 0; i < parameterTypes.length; i++) {
            Annotation[] paramAnnotations = annotations[i];
            Class paramClass = parameterTypes[i];

            if (JFedConnection.class.isAssignableFrom(paramClass))
                parameters[i] = connection;
            else
                for (Annotation paramAnnotation : paramAnnotations)
                    if (Objects.equals(paramAnnotation.annotationType(), ApiMethodParameter.class)) {
                        ApiMethodParameter amp = (ApiMethodParameter) paramAnnotation;

                        String arg = null;
                        if (remainingArguments.isEmpty()) {
                            if (amp.required()) {
                                if (interactive)
                                    arg =
                                            IOUtils.askCommandLineInput("Enter " + methodName + " argument '" + amp.name() + "'");
                                else {
                                    System.err.println("Missing " + methodName + " argument '" + amp.name() + "'");
                                    throw new RuntimeException("Error in CLI");
                                }
                            } else
                                arg = null;
                            //throw new RuntimeException("Error: Not enough arguments specified for method "+methodName+". Syntax: "+methodName+" "+getMethodSyntax(methodName));
                        } else
                            arg = remainingArguments.pollFirst();

                        if (arg == null || arg.equals("null")) {
                            parameters[i] = null;

                            if (Objects.equals(amp.name(), "credentialList")) {
                                parameters[i] = Collections.emptyList();
                            }
                        } else {
                            try {
                                if (Objects.equals(amp.name(), "credentialList")) {
                                    List<AnyCredential> credList = new ArrayList<>();
                                    String cred = IOUtils.fileToString(arg);
                                    credList.add(AnyCredential.createSfa2("credential from file " + arg, cred));
                                    parameters[i] = credList;
                                } else if (Objects.equals(amp.name(), "urns")) {
                                    String[] helper = arg.split(",");
                                    List<String> urns = Arrays.asList(helper);
                                    parameters[i] = urns;
                                } else if (Objects.equals(amp.name(), "users")) {
                                    String xml = arg.trim();
                                    if (xml.isEmpty()) {
                                        parameters[i] = new ArrayList<UserSpec>();
                                    } else {
                                        List<UserSpec> userSpecs = null;
                                        try {
                                            UserSpecParser userSpecParser = new UserSpecParser(xml);

                                            userSpecs = userSpecParser.call();
                                        } catch (Exception e) {
                                            LOG.error("Error unmarhalling XML list", e);
                                            e.printStackTrace(System.err);

                                            String exampleXml =
                                                    "<user-list xmlns=\"http://jfed.iminds.be/rspec/ext/jfed-ssh-keys/1\">" +
                                                            "        <user-ssh-keys user=\"urn:publicid:IDN+example.com+user+tester\">\n" +
                                                            "           <sshkey>ssh-rsa foo</sshkey>\n" +
                                                            "       </user-ssh-keys>\n" +
                                                            "       <user-ssh-keys user=\"urn:publicid:IDN+example.com+user+tester2\">\n" +
                                                            "           <sshkey>ssh-rsa bar</sshkey>\n" +
                                                            "           <sshkey>ssh-rsa baz</sshkey>\n" +
                                                            "       </user-ssh-keys>\n" +
                                                            "</user-list>\n";
                                            System.err.println("\nError: Argument \"users\" must be empty, or the xml representation of a list of user specifications. Example of such an xml:\n" + exampleXml + "\n");
                                            throw new RuntimeException("Error in CLI");
                                        }
                                        parameters[i] = userSpecs;
                                    }
                                } else {
                                    if (Objects.equals(amp.name(), "rspec")) {
                                        parameters[i] = IOUtils.fileToString(arg);
                                    } else {
                                        if (Objects.equals(paramClass, String.class))
                                            parameters[i] = arg;
                                        else if (Objects.equals(paramClass, Integer.class))
                                            parameters[i] = Integer.parseInt(arg);
                                        else if (Objects.equals(paramClass, Boolean.class))
                                            parameters[i] = Boolean.parseBoolean(arg);
                                        else if (Objects.equals(paramClass, ResourceId.class))
                                            parameters[i] = ResourceIdParser.parse(arg);
                                        else if (Objects.equals(paramClass, ResourceUrn.class))
                                            parameters[i] = new ResourceUrn(arg);
                                        else if (Objects.equals(paramClass, AnyCredential.class) ||
                                                Objects.equals(paramClass, SfaCredential.class) ||
                                                Objects.equals(paramClass, AbacCredential.class)) {
                                            String cred = IOUtils.fileToString(arg);
                                            parameters[i] =
                                                    AnyCredential.createSfa2("credential from file " + arg, cred);
                                        } else {
                                            res.exitvalue = 3;
                                            throw new RuntimeException("ERROR: Implementation incomplete: Unsupported parameter type: " + paramClass.getName() + " (name=\"" + amp.name() + "\")");
                                        }
                                    }
                                }
                            } catch (IOException e) {
                                res.exitvalue = 3;
                                throw new RuntimeException("ERROR: IOException reading credential file: " + e.getMessage(), e);
                            } catch (CredentialException e) {
                                res.exitvalue = 3;
                                throw new RuntimeException("ERROR: CredentialException processing credential file: " + e.getMessage(), e);
                            }
                        }
                    }
        }

        try {
            Object methodRes = m.invoke(targetObject, parameters);
            ApiCallReply methodResRep = (ApiCallReply) methodRes;
            if (methodResRep.getGeniResponseCode().isSuccess()) {
                res.output += methodResRep.getValue().toString();
                res.exitvalue = 0;
            } else {
                res.output +=
                        "Error reported by server: " + methodResRep.getGeniResponseCode() + " output=" + methodResRep.getOutput();
                if (methodResRep.getGeniResponseCode().isBusy())
                    res.exitvalue = 99;
                else
                    res.exitvalue = 1;
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            res.exitvalue = 2;
        }

        return res;
    }
}
