package be.iminds.ilabt.jfed.lowlevel.api;

import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * AbstractGeniAggregateManager
 */
public abstract class AbstractGeniAggregateManager extends AbstractApi {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AbstractGeniAggregateManager.class);

    public AbstractGeniAggregateManager(@Nonnull Logger logger, @Nonnull RetrySettings retrySettings, @Nonnull ApiInfo.Api serverType, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, retrySettings, serverType, jFedPreferences);
    }

    protected <T> AggregateManagerReply<T> executeAndLogXmlRpcCommandGeni(@Nullable Map<String, Object> methodParams,
                                                                          @Nonnull SfaConnection con,
                                                                          @Nonnull String methodJavaName,
                                                                          @Nonnull String methodGeniName,
                                                                          @Nonnull List<Object> args,
                                                                          @Nonnull ReplyConverter<T> replyConverter) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(methodParams, con, methodJavaName, methodGeniName, args, replyConverter, null);
    }

    protected <T> AggregateManagerReply<T> executeAndLogXmlRpcCommandGeni(@Nullable Map<String, Object> methodParams,
                                                                          @Nonnull SfaConnection con,
                                                                          @Nonnull String methodJavaName,
                                                                          @Nonnull String methodGeniName,
                                                                          @Nonnull List<Object> args,
                                                                          @Nonnull ReplyConverter<T> replyConverter,
                                                                          @Nullable T defaultValue) throws JFedException {

        XMLRPCCallDetailsWithCodeValueError res = executeXmlRpcCommandGeni(con, methodGeniName, args, methodParams);
        Object resultValueObject = res.getResultValueObject();

        AggregateManagerReply<T> r;
        if (resultValueObject != null && !Objects.equals(resultValueObject, 0)) {
            try {
                if (replyConverter == null)
                    r = new AggregateManagerReply<>(res, null);
                else
                    r = new AggregateManagerReply<>(res, replyConverter.convertXMLRPCCall(resultValueObject));
            } catch (AssertionError | Exception t) {
                handleErrorProcessingArguments(res, methodJavaName, methodGeniName, con, t);
                r = null;
            }
        } else {
            r = null;
        }

        if (r == null)
            r = new AggregateManagerReply<>(res, defaultValue);
        log(res, r, methodJavaName, methodGeniName, con, methodParams);
        return r;
    }

    protected void handleErrorProcessingArguments(XMLRPCCallDetailsWithCodeValueError res,
                                                  String methodJavaName, String methodGeniName,
                                                  SfaConnection con, Throwable t) throws JFedException {
        if (isCallSuccess(res)) {
            //The call was reportedly a success, but the parsing of the call failed. We should throw an error
            log(res, null, methodJavaName, methodGeniName, con, null);
            throw new JFedException("Error parsing " + methodGeniName + " reply: " + (t == null ? "null"
                    : t.getMessage()), t, res);
        } else {
            //If call failed, error may be ignored.
            LOG.error("Error parsing {} reply: {}", methodGeniName, t.getMessage(), t);
        }
    }

    private static boolean isCallSuccess(XMLRPCCallDetailsWithCodeValueError res) {
        Object resultCode = res.getResultCode();
        if (!(resultCode instanceof Map)) {
            return false;
        }
        Map codeStruct = (Map) res.getResultCode();
        if (codeStruct == null) {
            return false;
        }
        Object geniCode = codeStruct.get("geni_code");
        if (!(geniCode instanceof Integer)) {
            return false;
        }
        try {
            GeniResponseCode genicode = GeniAMResponseCode.getByCode((Integer) geniCode);
            return genicode.isSuccess();
        } catch (Exception ignored) {
            return false;
        }
    }

    @Override
    protected boolean isBusyReply(be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
            XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;

            if (geniDetails.getResultCode() == null ||
                    !(geniDetails.getResultCode() instanceof Map))
                return false;

            Map codeStruct = (Map) geniDetails.getResultCode();
            int code = (Integer) codeStruct.get("geni_code");

            try {
                if (GeniAMResponseCode.getByCode(code).isBusy())
                    return true;
            } catch (UnknownResponseCodeException e) {
                LOG.error("Did not recognise AM response code to determine if it is busy", e);
            }

            //exception:  (see FEDIBBTDEV-980)
            //   ION might return geni_code 5 (server error) when renew is not possible, with output text:
            //       ": RenewSliver: Internal API error: RSpec 'urn:publicid:IDN+ch.geni.net:CHtest+slice+mix1' is in 'VLANS-CREATED' status -- cannot renew for now!"
            //   We will treat this as "busy"
            if (code == 5 &&
                    geniDetails.getResultOutput() != null &&
                    geniDetails.getResultOutput().contains("cannot renew for now"))
                return true;
        }
        return false;
    }

    @Override
    protected boolean isPossibleTemporaryErrorReply(be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
            XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;
            if (!(geniDetails.getResultCode() instanceof Map))
                return false;

            Map codeStruct = (Map) geniDetails.getResultCode();

            if (codeStruct == null || !codeStruct.containsKey("geni_code"))
                return false;

            int code = (Integer) codeStruct.get("geni_code");

            if (code == 3 &&
                    Objects.equals(geniDetails.getResultOutput().trim(), "Credential owner does not match speaksfor target")) {
                LOG.warn("Received \"Credential owner does not match speaksfor target\" error. Will handle this as \"possible temporary error\" and try again.");
                return true;
            }
        }
        return false;
    }

    @Nonnull
    public abstract AggregateManagerReply<? extends AbstractVersionInfo> getVersion(@Nonnull SfaConnection con) throws JFedException;

    public static class AggregateManagerReply<T> implements SfaApiCallReply<T> {
        private final GeniAMResponseCode genicode;
        private final T val;
        /* output is typically set only on error*/
        private final String output;

        private final Map rawResult;
        private final XMLRPCCallDetailsWithCodeValueError xMLRPCCallDetailsWithCodeValueError;

        public AggregateManagerReply(XMLRPCCallDetailsWithCodeValueError res, T val) {
            this.xMLRPCCallDetailsWithCodeValueError = res;
            this.rawResult = res.getResult();

            Map r = res.getResult();
            Map codeStruct = (Map) res.getResultCode();
            int code;
            if (codeStruct == null) {
                LOG.error("Failed to parse Geni AM reply code. codeStruct==null");
                code = GeniAMResponseCode.SERVER_REPLY_ERROR.getCode();
            } else {
                Object geniCode = codeStruct.get("geni_code");
                if (geniCode != null) {
                    if (geniCode instanceof Integer) {
                        code = (Integer) geniCode;
                    } else {
                        LOG.error("Failed to parse Geni AM reply code. codeStruct.get(\"geni_code\") is not Integer but a " + codeStruct.getClass().getName());
                        code = GeniAMResponseCode.SERVER_REPLY_ERROR.getCode();
                    }
                } else {
                    LOG.error("Failed to parse Geni AM reply code. codeStruct.get(\"geni_code\")==null");
                    code = GeniAMResponseCode.SERVER_REPLY_ERROR.getCode();
                }
            }

            GeniAMResponseCode genicode = null;
            try {
                genicode = GeniAMResponseCode.getByCode(code);
            } catch (UnknownResponseCodeException e) {
                LOG.error("Did not recognise response code", e);
            }
            this.genicode = genicode;

            String output = null; //this should be a string, but we allow more. This may be null on success.
            if (r.get("output") != null) {
                output = r.get("output").toString();
                //any empty structure is interpreted as an empty string
                if (r.get("output") instanceof Map && ((Map) r.get("output")).isEmpty())
                    output = "";
                if (r.get("output") instanceof Object[] && ((Object[]) r.get("output")).length ==0)
                    output = "";
            }
            this.output = output;

            this.val = val;
        }

        public static boolean isSuccess(be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
            if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
                XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;
                Map codeStruct = (Map) geniDetails.getResultCode();
                int code = (Integer) codeStruct.get("geni_code");
                try {
                    return GeniAMResponseCode.getByCode(code).isSuccess();
                } catch (UnknownResponseCodeException e) {
                    LOG.error("Did not recognise response code when determining success", e);
                    return false;
                }
            } else
                return false; //not a geni reply, so not successful
        }

        public int getCode() {
            return genicode.getCode();
        }

        public GeniAMResponseCode getGeniResponseCode() {
            return genicode;
        }

        public T getValue() {
            return val;
        }

        /**
         * can be null
         */
        public String getOutput() {
            return output;
        }

        @Override
        public XMLRPCCallDetailsWithCodeValueError getXMLRPCCallDetailsWithCodeValueError() {
            return xMLRPCCallDetailsWithCodeValueError;
        }

        @Override
        public XMLRPCCallDetails getXMLRPCCallDetails() {
            return xMLRPCCallDetailsWithCodeValueError;
        }

        public Map getRawResult() {
            return rawResult;
        }

        @Override
        public Object getRawValue() {
            if (rawResult == null)
                return null;
            return rawResult.get("value");
        }

    }

    public static abstract class AbstractVersionInfo {
        public abstract int getApi();
    }

    public static boolean isNotEnoughFreeResources(@Nullable AggregateManagerReply reply) {
        if (reply == null)
            return false;
        if (reply.getGeniResponseCode().isSuccess())
            return false;
        if (reply.getGeniResponseCode().isBusy())
            return false;
        if (reply.getOutput() == null)
            return false;

        String output = reply.getOutput().toLowerCase();

        //Some AM's, like emulab, add a string value when an error occurs. It can't hurt to include that in this search as well.
        if (reply.getValue() != null && reply.getValue() instanceof String)
            output += ((String) reply.getValue()).toLowerCase();
        else {
            if (reply.getRawValue() != null && reply.getRawValue() instanceof String)
                output += ((String) reply.getRawValue()).toLowerCase();
        }

        if (output.contains("no available resources".toLowerCase())) //emulab and instageni
            return true;
        if (output.contains("could not map to resources".toLowerCase()))  //exogeni     (also emulab in value instead of output)
            return true;
        if (output.contains("free resources".toLowerCase()))
            return true;
        if (output.contains("failed to map".toLowerCase()))
            return true;
        if (output.contains("available resources".toLowerCase()))
            return true;
        if (output.contains("no possible mapping".toLowerCase())) //emulab   (example: " *** No possible mapping for PC\n")
            return true;
        if (output.contains("ERROR: mapper: Unretriable error. Giving up".toLowerCase()))
            return true;
        if (output.contains("No available physical nodes of type ".toLowerCase()))
            return true;
        if (output.contains("insufficient resources to fulfill request".toLowerCase()))
            return true;
        if (output.contains("Not enough resources".toLowerCase())) //exogeni:  FEDIBBTDEV-2033
            return true;
        if (output.contains("insufficient resources available".toLowerCase())) //log-a-tec
            return true;

        return false;
    }
}
