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.*;

/**
 * <p>This is a simple wrapper for the methods specified in the {@link <a href="http://www.protogeni.net/ProtoGeni/wiki/ClearingHouseAPI1">ProtoGeni Clearing House API v1</a>}.
 * <p>All functionality provided by the API is available in this implementation, and little processing of results is done.
 * This class stores no internal state, except for the logger used. It sends all results and debug info to this {@link Logger} if it is non null.
 * Only a few methods are implemented, as most do not make sense for a client. (You can use the existing methods as a template and example to extend this class if other methods are needed)
 * <p/>
 * <p>All methods require a {@link be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection} argument and return a {@link ProtoGeniClearingHouse1.ClearingHouseReply} which wraps the actual return value of the call, if any.
 *
 * @author Wim Van de Meerssche
 * @see <a href="http://www.protogeni.net/ProtoGeni/wiki/ClearingHouseAPI1">ProtoGeni Clearing House API v1</a>
 */
public class ProtoGeniClearingHouse1 extends AbstractApi {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ProtoGeniClearingHouse1.class);

    /**
     * A human readable name for the implemented API
     *
     * @return "ProtoGeni Clearing House API v1";
     */
    static public String getApiName() {
        return "ProtoGeni Clearing House API v1";
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "ProtoGeni Clearing House API v1";
     */
    @Override
    public String getName() {
        return getApiName();
    }

    /**
     * Construct a new ClearingHouse1, with a certain logger.
     * <p/>
     * <p>This class stores no internal state, except for the logger used. It sends all results and debug info to this {@link Logger} if it is non null.
     *
     * @param logger        the logger to use. May be null if no logger is used.
     * @param retrySettings the retrySettings to use, or null to use the static globalRetrySettings in AbstractApi
     */
    public ProtoGeniClearingHouse1(@Nonnull Logger logger, @Nonnull RetrySettings retrySettings, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, retrySettings, new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_CH, 1), jFedPreferences);
    }

    /**
     * Construct a new ClearingHouse1, with a certain logger.
     * <p/>
     * <p>This class stores no internal state, except for the logger used. It sends all results and debug info to this {@link Logger} if it is non null.
     * <p/>
     * <p>This constructor sets autoRetryBusy to true, so "busy" replies are retried.
     *
     * @param logger the logger to use. May be null if no logger is used.
     */
    public ProtoGeniClearingHouse1(@Nonnull Logger logger, @Nonnull JFedPreferences jFedPreferences) {
        this(logger, jFedPreferences.getRetrySettings(), jFedPreferences);
    }

    @Override
    protected boolean isBusyReply(be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
            XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;
            int code = (Integer) geniDetails.getResultCode();
            try {
                return GeniAMResponseCode.getByCode(code).isBusy();
            } catch (UnknownResponseCodeException e) {
                LOG.warn("Did not recognise AM response code to determine if it is busy", e);
                return false;
            }
        } else
            return false;
    }

    protected <T> ClearingHouseReply<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> ClearingHouseReply<T> executeAndLogXmlRpcCommandGeni(Map<String, Object> methodParams,
                                                                       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();
        ClearingHouseReply<T> r;
        if (resultValueObject != null && !Objects.equals(resultValueObject, 0)) {
            try {
                r = new ClearingHouseReply<>(res, replyConverter.convertXMLRPCCall(resultValueObject));
            } catch (AssertionError | Exception t) {
                handleErrorProcessingArguments(res, methodJavaName, methodGeniName, con, t);
                r = null;
            }
        } else
            r = null;

        if (r == null)
            r = new ClearingHouseReply<>(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;
        }
    }


    public static class ClearingHouseReply<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 xmlRpcDetails;

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

        public GeniAMResponseCode getGeniResponseCode() {
            return genicode;
        }

        public T getValue() {
            return val;
        }

        public String getOutput() {
            return output;
        }

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

        public ClearingHouseReply(XMLRPCCallDetailsWithCodeValueError res, T val) {
            this.xmlRpcDetails = res;
            this.rawResult = res.getResult();
            int intCode = (Integer) res.getResultCode();

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

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

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

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


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


    /**
     * Returns an integer corresponding to the revision of this API supported by the clearing house.
     *
     * @param con the {@code GeniConnection} to use for this call. Must be a connection to a clearinghouse.
     * @return an integer corresponding to the revision of this API supported by the clearing house.
     */
    @ApiMethod(order = 1)
    @Nonnull
    public ClearingHouseReply<Integer> getVersion(@Nonnull SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getVersion", "get_version",
                Collections.emptyList(), resultValueObject -> {
                    if (resultValueObject != null) {
                        if (resultValueObject instanceof Integer)
                            return (Integer) resultValueObject;
                        else
                            throw new BadReplyGeniException("Expected an integer, but got a " + resultValueObject.getClass().getName() + " as result");
                    } else
                        return null;
                });
    }

    /**
     * Request a credential for accessing certain protected parts of the clearinghouse API.
     *
     * @param con the {@code GeniConnection} to use for this call. Must be a connection to a clearinghouse.
     * @return a credential for accessing certain protected parts of the clearinghouse API.
     */
    @ApiMethod(order = 2)
    public ClearingHouseReply<AnyCredential> getCredential(@Nonnull SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getCredential", "GetCredential",
                Collections.emptyList(), resultValueObject -> {
                    if (resultValueObject != null) {
                        try {
                            return AnyCredential.createSfa2("ClearingHouse getCredential", resultValueObject.toString());
                        } catch (CredentialException e) {
                            throw new BadReplyGeniException("Exception processing credential: " + e.getMessage(), e);
                        }
                    } else
                        throw new BadReplyGeniException("Expected a ClearingHouse Credential, but got null");
                });
    }

    public static class ComponentInfo {
        private final Gid gid;
        private final String hrn;
        private final String url;
        private final String urn;

        public ComponentInfo(Map ht) {
            this.gid = new Gid(ht.get("gid").toString());
            this.hrn = ht.get("hrn").toString();
            this.url = ht.get("url").toString();
            this.urn = ht.get("urn").toString();
        }

        public Gid getGid() {
            return gid;
        }

        public String getHrn() {
            return hrn;
        }

        public String getUrl() {
            return url;
        }

        public String getUrn() {
            return urn;
        }

        @Override
        public String toString() {
            return "ComponentInfo{" +
                    "gid='" + gid + '\'' +
                    ", hrn='" + hrn + '\'' +
                    ", url='" + url + '\'' +
                    '}';
        }
    }

    /**
     * Return a list of all component managers. This is typically used by a client to find out where the managers are,
     * so that it can ask the managers about the specific resources it manages.
     *
     * @param con        the {@code GeniConnection} to use for this call. Must be a connection to a clearinghouse.
     * @param credential a valid user credential. This is typically the credential one receives when calling
     *                   {@link ProtogeniSliceAuthority#getCredential(be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection)}
     * @return a list of aggregate manager info.
     */
    @ApiMethod(order = 3)
    public ClearingHouseReply<List<ComponentInfo>> listComponents(@Nonnull SfaConnection con,
                                                                  @ApiMethodParameter(name = "userCredential")
                                                                  @Nonnull AnyCredential credential) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("userCredential", credential);

        Map<String, String> params = new HashMap<>();
        params.put("credential", credential.getCredentialXml());

        List<Object> args = Collections.singletonList(params);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "listComponents", "ListComponents",
                args, resultValueObject -> {
                    List<Map> componentsV = apiSpecifiesListOfT(Map.class, resultValueObject);
                    List<ComponentInfo> compList = new ArrayList<>();

                    for (Map ht : componentsV) {
                        compList.add(new ComponentInfo(ht));
                    }

                    return compList;
                });
    }


    //These are not interesting to implement on the client

//    /**
//     * Register a principle object.
//     *    Note: Slice authorities register the slices they create, as well as the users who create those slices.
//     *          SAs and CMs are registered with the clearinghouse as part of the process of joining the federation.
//     *          Component Managers (will, but do not yet) register the components they manage.
//     * @param con
//     * @param credential the credential returned by GetCredential()
//     * @param gid the gid (public key) of the object to be registered,
//     * @param type one of SA|CM|Component|Slice|User
//     * @param info an array of additional arguments specific to the principle being registered
//     * @return success
//     * @throws GeniException
//     */
//    @ApiMethod(order=4)
//    public ClearingHouseReply<Boolean> register(GeniConnection con, AnyCredential credential, String gid, String type, Map info) throws GeniException {
//        List args = new List();
//        args.setSize(4);
//        args.set(0, credential.getCredentialXml());
//        args.set(1, gid);
//        args.set(2, type);
//        args.set(3, info);
//        XMLRPCHelper.XMLRPCHelperResult res = XMLRPCHelper.execute(con, "GetCredential", args);
//        log(res);
//        return new ClearingHouseReply<Boolean>(res);
//    }
//
//
//
//    private ClearingHouseReply<Map> resolve(GeniConnection con, AnyCredential credential, String resource, String type) throws GeniException {
//        XMLRPCHelper.XMLRPCHelperResult res = XMLRPCHelper.execute(con, "GetCredential", new List());
//        log(res);
//        return new ClearingHouseReply<Map>(res);
//    }
//
//    @ApiMethod(order=5)
//    public ClearingHouseReply<Map> resolve(GeniConnection con, AnyCredential credential, UuidResource uuid, String type) throws GeniException {
//        return resolve(con, credential, uuid.getValue(), type);
//    }
//
//    @ApiMethod(order=6)
//    public ClearingHouseReply<Map> resolve(GeniConnection con, AnyCredential credential, HrnResource hrn, String type) throws GeniException {
//        return resolve(con, credential, hrn.getValue(), type);
//    }
//TODO Remove(credential, uuid, type)
//TODO Shutdown(credential, uuid);
//TODO PostCRL(credential, certificate);
//TODO List(credential,type);

}
