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 be.iminds.ilabt.jfed.util.RFC3339Util;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;

/**
 * StitchingComputationService
 * <p/>
 * see https://dragon.maxgigapop.net/twiki/bin/view/GENI/NetworkStitchingAPI
 */
public class StitchingComputationService extends AbstractApi {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(StitchingComputationService.class);

    public StitchingComputationService(@Nonnull Logger logger, @Nonnull JFedPreferences jFedPreferences) {
        this(logger, jFedPreferences.getRetrySettings(), jFedPreferences);
    }

    public StitchingComputationService(@Nonnull Logger logger, RetrySettings retrySettings, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, retrySettings, new ApiInfo.Api(ApiInfo.ApiName.GENI_SCS, 1), jFedPreferences);
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "Stitching Computation Service";
     */
    static public String getApiName() {
        return "Stitching Computation Service";
    }

    @Override
    protected boolean isBusyReply(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 SCSResponseCode.getByCode(code).isBusy();
            } catch (UnknownResponseCodeException e) {
                LOG.warn("Did not recognise AM response code to determine if it is busy", e);
                return false;
            }
        }
        return false;
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "Stitching Computation Service";
     */
    @Override
    public String getName() {
        return getApiName();
    }

    @ApiMethod(order = 1, hint = "GetVersion call: Get static version and configuration information about this SCS. There is no documentation about this call, but it does exist.")
    public SCSReply<Map> getVersion(SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getVersion", "GetVersion",
                new ArrayList<>(), AbstractApi::apiSpecifiesMapStringToObject);
    }

    @ApiMethod(order = 2, hint = "ComputePath call: Compute a Network Stitching Path. See https://wiki.maxgigapop.net/twiki/bin/view/GENI/NetworkStitchingAPI")
    public SCSReply<ComputePathResult> computePath(SfaConnection con,
                                                   @ApiMethodParameter(name = "sliceUrn", hint = "The slice on which everything needs to be created", parameterType = ApiMethodParameterType.SLICE_URN)
                                                   @Nonnull String sliceUrn,
                                                   @ApiMethodParameter(name = "requestRspec", hint = "request rspec with or without stitching extensions", parameterType = ApiMethodParameterType.RSPEC_STRING)
                                                   @Nonnull String requestRspec,
                                                   @ApiMethodParameter(name = "requestOptions", required = false, hint = "optional options")
                                                   @Nullable Map<String, Object> requestOptions) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("sliceUrn", sliceUrn,
                "requestRspec", requestRspec,
                "requestOptions", requestOptions);
        //this is documented
//        List args = new List(3);
//        args.add(sliceUrn);
//        args.add(requestRspec);
//        if (requestOptions == null)
//            args.add(new Map());
//        else
//            args.add(requestOptions);

        //but this is what it really is
        Map<String, Object> args = new HashMap<>();
        args.put("slice_urn", sliceUrn);
        args.put("request_rspec", requestRspec);
        if (requestOptions != null) {
            args.put("request_options", requestOptions);
        } else {
            args.put("request_options", Collections.emptyMap());
        }

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "computePath", "ComputePath",
                singletonList(args), StitchingComputationService::processComputePathResultReply);
    }

    @Nonnull
    private static ComputePathResult processComputePathResultReply(Object resultValueObject) throws BadReplyGeniException {
        Map<String, Object> resultMap = apiSpecifiesMapStringToObject(resultValueObject);

        String rspec = null;

        try {
            rspec = apiSpecifiesNonNullString(resultMap.get("service_rspec"));
        } catch (BadReplyGeniException e) {
            LOG.warn("SCS ComputePath service_rspec could not be extracted from reply", e);

        }

        Map workFlowData = null;
        try {
            workFlowData = apiSpecifiesMapStringToObject(resultMap.get("workflow_data"));
        } catch (BadReplyGeniException e) {
            LOG.warn("SCS WorkFlow could not be extracted from reply", e);
            e.printStackTrace();
        }

        return new ComputePathResult(workFlowData, rspec);
    }


    @Nonnull
    private static List<Object> singletonList(Object value) {
        List<Object> args = new ArrayList<>(1);
        args.add(value);
        return args;
    }


    @SuppressWarnings("unchecked")
    @ApiMethod(order = 2, hint = "ComputePath call with extra options: Compute a Network Stitching Path. See https://wiki.maxgigapop.net/twiki/bin/view/GENI/NetworkStitchingAPI")
    public SCSReply<ComputePathResult> computePathWithOptions(SfaConnection con,
                                                              @ApiMethodParameter(name = "sliceUrn", hint = "The slice on which everything needs to be created", parameterType = ApiMethodParameterType.SLICE_URN)
                                                              @Nonnull String sliceUrn,
                                                              @ApiMethodParameter(name = "requestRspec", hint = "request rspec with or without stitching extensions", parameterType = ApiMethodParameterType.RSPEC_STRING)
                                                              @Nonnull String requestRspec,
                                                              @ApiMethodParameter(name = "requestOptions", required = false, hint = "optional options")
                                                              @Nullable Map<String, Object> requestOptions,

                                                              //known requestOptions
                                                              @ApiMethodParameter(name = "geniHoldPath", required = false, guiDefault = "false", hint = "optional, default=false. SCS to hold the path exclusively for a preconfigured holding period to avoid contention")
                                                              @Nullable Boolean geniHoldPath,
                                                              @ApiMethodParameter(name = "geniStartTime", required = false, hint = "optional, default=now, projected starting time for the stitching path(s)")
                                                              @Nullable Date geniStartTime,
                                                              @ApiMethodParameter(name = "geniEndTime", required = false, hint = "optional, default=infinite, projected expiration time for the stitching path(s)")
                                                              @Nullable Date geniEndTime,
                                                              @ApiMethodParameter(name = "geniRoutingProfile", required = false, guiDefault = "{ 'hop_inclusion_list': [] , 'hop_exclusion_list': [] }", hint = "optional,  a struct of {path_id: routing_profile} map. The path_id corresponding to a stitching path id in request_rspec. routing_profile is a struct of two optional items {'hop_inclusion_list': array of urn strings} and {'hop_exclusion_list': array of urn strings}.")
                                                              @Nullable Map geniRoutingProfile,
                                                              @ApiMethodParameter(name = "geniWorkflowData", required = false, guiDefault = "true", hint = "optional, default=true, SCS to return workflow data in support of user / agent / tool workflow logic.")
                                                              @Nullable Boolean geniWorkflowData) throws JFedException {
        if (requestOptions == null)
            requestOptions = new HashMap<>();


        addOptionToRequest(geniHoldPath, "geni_hold_path", requestOptions);
        addOptionToRequest(
                geniStartTime != null ? RFC3339Util.dateToRFC3339String(geniStartTime, true/*forceZuluhack*/) : null,
                "geni_start_time", requestOptions);
        addOptionToRequest(
                geniEndTime != null ? RFC3339Util.dateToRFC3339String(geniEndTime, true/*forceZuluhack*/) : null,
                "geni_end_time", requestOptions);
        addOptionToRequest(geniRoutingProfile, "geni_routing_profile", requestOptions);
        addOptionToRequest(geniWorkflowData, "geni_workflow_data", requestOptions);

        return computePath(con, sliceUrn, requestRspec, requestOptions);
    }

    private static void addOptionToRequest(Object fieldValue, String fieldName, Map<String, Object> requestOptions) {
        if (fieldValue != null) {
            if (requestOptions.containsKey(fieldName))
                throw new IllegalArgumentException(fieldName + " was already declared in requestOptions.");
            requestOptions.put(fieldName, fieldValue);
        }
    }

    public static class SCSReply<T> implements SfaApiCallReply<T> {
        private final XMLRPCCallDetailsWithCodeValueError xmlRpcCallDetailsWithCodeValueError;
        private final SCSResponseCode genicode;
        private final T val;
        /* output is typically set only on error*/
        private final String output;
        private final Map rawResult;

        public SCSReply(XMLRPCCallDetailsWithCodeValueError res, T val) {
            this.xmlRpcCallDetailsWithCodeValueError = res;
            this.rawResult = res.getResult();

            Map code = (Map) res.getResultCode();
            int intCode = (Integer) code.get("geni_code");
            SCSResponseCode genicode = null;
            try {
                genicode = SCSResponseCode.getByCode(intCode);
            } catch (UnknownResponseCodeException e) {
                LOG.warn("Did not recognise AM response code", e);
            }
            this.genicode = genicode;

            this.val = val;
            this.output = res.getResultOutput();
        }

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

        public SCSResponseCode getGeniResponseCode() {
            return genicode;
        }

        public T getValue() {
            return val;
        }

        public String getOutput() {
            return output;
        }

        @Override
        public Map getRawResult() {
            return rawResult;
        }

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

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


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

    public static class ComputePathResult {
        @Nullable
        private final String serviceRspec;
        @Nullable
        private final Map workflowData;

        public ComputePathResult(@Nullable Map workflowData, @Nullable String serviceRspec) {
            this.workflowData = workflowData;
            this.serviceRspec = serviceRspec;
        }

        @Nullable
        public String getServiceRspec() {
            return serviceRspec;
        }

        @Nullable
        public Map getWorkflowData() {
            return workflowData;
        }

        @Override
        public String toString() {
            return "ComputePathResult{" +
                    "serviceRspec='" + serviceRspec + '\'' +
                    ", workflowData=" + workflowData +
                    '}';
        }
    }

    protected <T> SCSReply<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> SCSReply<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();
        SCSReply<T> r;

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

        if (r == null)
            r = new SCSReply<>(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 Integer)) {
            return false;
        }
        try {
            GeniResponseCode genicode = GeniAMResponseCode.getByCode((Integer) resultCode);
            return genicode.isSuccess();
        } catch (Exception ignored) {
            return false;
        }
    }
}
