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.GeniUrn;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.*;

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

    /**
     * A human readable name for the implemented API
     *
     * @return "Uniform Federation Member Authority API v1";
     */
    static public String getApiName() {
        return "Uniform Federation Member Authority API v1";
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "Uniform Federation Member Authority API v1";
     */
    @Override
    public String getName() {
        return getApiName();
    }

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

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

    public String getMethodObject(Method method) {
        if (Objects.equals(method.getName(), "createKey") ||
                Objects.equals(method.getName(), "lookupKeys") ||
                Objects.equals(method.getName(), "deleteKey") ||
                Objects.equals(method.getName(), "updateKey"))
            return "KEY";

        if (Objects.equals(method.getName(), "lookupPublicMemberInfo") ||
                Objects.equals(method.getName(), "lookupPrivateMemberInfo") ||
                Objects.equals(method.getName(), "lookupIdentifyingMemberInfo") ||
                Objects.equals(method.getName(), "updateMemberInfo"))
            return "MEMBER";

        return null;
    }

    private static final List<GetVersionResult.FieldInfo> MINIMUM_MEMBER_FIELDS =
            Collections.unmodifiableList(Arrays.asList(
                    new GetVersionResult.FieldInfo("MEMBER", "MEMBER_URN", GetVersionResult.FieldInfo.FieldType.URN, true, GetVersionResult.FieldInfo.Protect.PUBLIC),
                    new GetVersionResult.FieldInfo("MEMBER", "MEMBER_UID", GetVersionResult.FieldInfo.FieldType.UID, true, GetVersionResult.FieldInfo.Protect.PUBLIC),
                    new GetVersionResult.FieldInfo("MEMBER", "MEMBER_FIRSTNAME", GetVersionResult.FieldInfo.FieldType.STRING, true, GetVersionResult.FieldInfo.Protect.IDENTIFYING),
                    new GetVersionResult.FieldInfo("MEMBER", "MEMBER_LASTNAME", GetVersionResult.FieldInfo.FieldType.STRING, true, GetVersionResult.FieldInfo.Protect.IDENTIFYING),
                    new GetVersionResult.FieldInfo("MEMBER", "MEMBER_USERNAME", GetVersionResult.FieldInfo.FieldType.STRING, true, GetVersionResult.FieldInfo.Protect.PUBLIC),
                    new GetVersionResult.FieldInfo("MEMBER", "MEMBER_EMAIL", GetVersionResult.FieldInfo.FieldType.STRING, false, GetVersionResult.FieldInfo.Protect.IDENTIFYING)
            ));

    private static final List<GetVersionResult.FieldInfo> MINIMUM_KEY_FIELDS =
            Collections.unmodifiableList(Arrays.asList(
                    new GetVersionResult.FieldInfo("KEY", "KEY_MEMBER", GetVersionResult.FieldInfo.FieldType.URN, GetVersionResult.FieldInfo.CreationAllowed.REQUIRED, true, false),
                    new GetVersionResult.FieldInfo("KEY", "KEY_ID", GetVersionResult.FieldInfo.FieldType.STRING, GetVersionResult.FieldInfo.CreationAllowed.NOT_ALLOWED, true, false),
                    new GetVersionResult.FieldInfo("KEY", "KEY_PUBLIC", GetVersionResult.FieldInfo.FieldType.KEY, GetVersionResult.FieldInfo.CreationAllowed.REQUIRED, true, false),
                    new GetVersionResult.FieldInfo("KEY", "KEY_PRIVATE", GetVersionResult.FieldInfo.FieldType.KEY, GetVersionResult.FieldInfo.CreationAllowed.ALLOWED, true, false),
                    new GetVersionResult.FieldInfo("KEY", "KEY_DESCRIPTION", GetVersionResult.FieldInfo.FieldType.STRING, GetVersionResult.FieldInfo.CreationAllowed.ALLOWED, true, true)
            ));


    @Nonnull
    @Override
    public List<GetVersionResult.FieldInfo> getMinimumFields(String objectName) {

        if (objectName.equalsIgnoreCase("MEMBER")) {
            return MINIMUM_MEMBER_FIELDS;
        } else if (objectName.equalsIgnoreCase("KEY")) {
            return MINIMUM_KEY_FIELDS;
        } else
            return Collections.emptyList();
    }

    private static final List<String> API_OBJECTS = Collections.unmodifiableList(Arrays.asList("MEMBER", "KEY"));

    @Override
    public List<String> getApiObjects() {
        return API_OBJECTS;
    }

    @Override
    public List<String> getRequiredApiServices() {
        return Collections.singletonList("MEMBER");
    }

    @Override
    public List<String> getOptionalApiServices() {
        return Collections.singletonList("KEY");
    }


    @ApiMethod(order = 1, hint = "get_version call: Provide a structure detailing the version information as well as details of accepted options s for CH API calls.", unprotected = true)
    @Nonnull
    public FederationApiReply<GetVersionResult> getVersion(@Nonnull SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getVersion", "get_version",
                new ArrayList<>(), resultValueObject -> new GetVersionResult(resultValueObject, false));
    }


    @ApiMethod(order = 2, hint = "lookup_public_member_info call: Lookup public information about members matching given criteria", unprotected = true)
    @Nonnull
    public FederationApiReply<MemberInfoList> lookupPublicMemberInfo(@Nonnull SfaConnection con,
                                                                     @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                   @Nullable List<AnyCredential> credentialList,
                                                                     @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                   @Nullable Map<String, ?> match,
                                                                     @ApiMethodParameter(name = "filter", parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                   @Nullable List<String> filter,
                                                                     @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                                   @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "match", match,
                        "filter", filter,
                        "extraOptions", extraOptions);
        return internalLookup(methodParams, con, "lookupPublicMemberInfo", "lookup_public_member_info",
                credentialList, match, filter, extraOptions, null, MemberInfoList::new);
    }

    @ApiMethod(order = 3, hint = "lookup_private_member_info call: Lookup private (SSL/SSH key) information about members matching given criteria")
    @Nonnull
    public FederationApiReply<MemberInfoList> lookupPrivateMemberInfo(@Nonnull SfaConnection con,
                                                                      @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                    @Nonnull List<AnyCredential> credentialList,
                                                                      @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                    @Nullable Map<String, ?> match,
                                                                      @ApiMethodParameter(name = "filter", parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                    @Nullable List<String> filter,
                                                                      @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                                    @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "match", match,
                        "filter", filter,
                        "extraOptions", extraOptions);
        return internalLookup(methodParams, con, "lookupPrivateMemberInfo", "lookup_private_member_info",
                credentialList, match, filter, extraOptions, null, MemberInfoList::new);
    }

    @ApiMethod(order = 4, hint = "lookup_identifying_member_info call: Lookup identifying (e.g. name, email) info about matching members")
    @Nonnull
    public FederationApiReply<MemberInfoList> lookupIdentifyingMemberInfo(@Nonnull SfaConnection con,
                                                                          @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                        @Nonnull List<AnyCredential> credentialList,
                                                                          @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                        @Nullable Map<String, ?> match,
                                                                          @ApiMethodParameter(name = "filter", parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                        @Nullable List<String> filter,
                                                                          @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                                        @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "match", match,
                        "filter", filter,
                        "extraOptions", extraOptions);
        return internalLookup(methodParams, con, "lookupIdentifyingMemberInfo", "lookup_identifying_member_info",
                credentialList, match, filter, extraOptions, null, MemberInfoList::new);
    }


    @ApiMethod(order = 5, hint = "update_member_info call: Update information about given member public, private or identifying information")
    @Nonnull
    public FederationApiReply<String> updateMemberInfo(@Nonnull SfaConnection con,
                                                       @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                       @Nonnull List<AnyCredential> credentialList,
                                                       @ApiMethodParameter(name = "urn", parameterType = ApiMethodParameterType.USER_URN)
                                                       @Nonnull String urn,
                                                       @ApiMethodParameter(name = "fields", parameterType = ApiMethodParameterType.CH_API1_FIELDS)
                                                       @Nonnull Map<String, String> fields,
                                                       @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                       @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "urn", urn,
                        "fields", fields,
                        "extraOptions", extraOptions);
        return genericUpdateCall(methodParams, con, "updateMemberInfo", "update_member_info", credentialList, urn, "member", fields, extraOptions);
    }


    /**  */
    @ApiMethod(order = 6, hint = "get_credentials call: Provide list of credentials (signed statements) for given member\n" +
            "This is member-specific information suitable for passing as credentials in an AM API call for aggregate authorization.")
    @Nonnull
    public FederationApiReply<List<AnyCredential>> getCredentials(@Nonnull SfaConnection con,
                                                                  @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                  @Nonnull List<AnyCredential> credentialList,
                                                                  @ApiMethodParameter(name = "memberUrn", parameterType = ApiMethodParameterType.USER_URN)
                                                                  @Nonnull GeniUrn memberUrn,
                                                                  @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                                  @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "memberUrn", memberUrn,
                        "extraOptions", extraOptions);

        if (!Objects.equals(memberUrn.getEncodedResourceType(), "user"))
            LOG.warn("member URN argument to getCredentials is not a valid member urn: \"" + memberUrn + "\" (will be used anyway)");

        List<Object> args = new ArrayList<>(3);
        args.add(memberUrn.toString());

        args.add(createCredentialsListWithTypeAndVersion(credentialList));
        args.add(createOptionsMap(extraOptions));

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "getCredentials", "get_credentials",
                args, resultValueObject -> apiSpecifiesListOfCredentials("FederationMemberAuthorityApi1 getCredentials", resultValueObject));
    }


    @ApiMethod(order = 7, hint = "lookup_keys call:")
    @Nonnull
    public FederationApiReply<Map<GeniUrn, List<MemberKeyInfo>>> lookupKeys(@Nonnull SfaConnection con,
                                                                                  @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                                  @Nonnull List<AnyCredential> credentialList,
                                                                                  @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                                  @Nullable Map<String, ?> match,
                                                                                  @ApiMethodParameter(name = "filter", parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                                  @Nullable List<String> filter,
                                                                                  @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                                                  @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "match", match,
                        "filter", filter,
                        "extraOptions", extraOptions);
        /*
         * This is not a generic lookup call: it returns:
         *
         *     I think that this call should return a list of dictionaries (one for each key) for each person indexed by member_urn.
         *     Put another way, a dictionary (indexed by member_urn) of lists of dictionaries (key fields).

         That is, for example,
         {
         member_urn_1 : [
         {'KEY_MEMBER' : member_urn_1, 'KEY_ID' : key_id_1_A, 'KEY_PUBLIC' : key_public_1_A, 'KEY_PRIVATE' : key_private_1_A, 'KEY_DESCRIPTION' : key_description_1_A},
         {'KEY_MEMBER' : member_urn_1, 'KEY_ID' : key_id_1_B, 'KEY_PUBLIC' : key_public_1_B, 'KEY_PRIVATE' : key_private_1_B, 'KEY_DESCRIPTION' : key_description_1_B}
         ],
         member_urn_2 : [
         {'KEY_MEMBER' : member_urn_2, 'KEY_ID' : key_id_2_A, 'KEY_PUBLIC' : key_public_2_A, 'KEY_PRIVATE' : key_private_2_A, 'KEY_DESCRIPTION' : key_description_2_A},
         {'KEY_MEMBER' : member_urn_2, 'KEY_ID' : key_id_2_B, 'KEY_PUBLIC' : key_public_2_B, 'KEY_PRIVATE' : key_private_2_B, 'KEY_DESCRIPTION' : key_description_2_B}
         ],
         ...
         }
         *
             so not   return genericLookup(con, "lookupKeys", "lookup_keys", credentialList, match, filter, extraOptions, null);
         **/
        List<Object> args = new ArrayList<>();

        args.add(createCredentialsListWithTypeAndVersion(credentialList));

        Map<String, Object> options = new HashMap<>();
        if (match != null)
            options.put("match", match);
        if (filter != null)
            options.put("filter", filter);
        if (extraOptions != null)
            options.putAll(extraOptions);
        args.add(options);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "lookupKeys", "lookup_keys",
                args, this::processLookupKeys);
    }

    private Map<GeniUrn, List<MemberKeyInfo>> processLookupKeys(Object resultValueObject) throws BadReplyGeniException {
        Map<GeniUrn, List<MemberKeyInfo>> resLookupResult = new HashMap<>();

        Map<String, Object> res2 = apiSpecifiesMapStringToObject(resultValueObject);
        for (Map.Entry<String, Object> o : res2.entrySet()) {
            GeniUrn urn;
            try {
                urn = new GeniUrn(o.getKey());
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new BadReplyGeniException("Could not parse received URN '" + o.getKey() + "'");
            }

            List<Map> o2 = apiSpecifiesListOfT(Map.class, o.getValue());
            List<MemberKeyInfo> covertedO2 = new ArrayList<>();
            for (Map keyInfo : o2) {
                covertedO2.add(new MemberKeyInfo(apiSpecifiesMapStringToString(keyInfo)));
            }

            resLookupResult.put(urn, covertedO2);
        }
        return resLookupResult;
    }

    @ApiMethod(order = 8, hint = "create_key call:")
    @Nonnull
    public FederationApiReply<MemberKeyInfo> createKey(@Nonnull SfaConnection con,
                                                                   @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                   @Nonnull List<AnyCredential> credentialList,
                                                                   @ApiMethodParameter(name = "memberUrn", parameterType = ApiMethodParameterType.USER_URN)
                                                                   @Nullable GeniUrn memberUrn,
                                                                   @ApiMethodParameter(name = "fields", parameterType = ApiMethodParameterType.CH_API1_FIELDS)
                                                                   @Nullable Map<String, String> fields,
                                                                   @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                                   @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "memberUrn", memberUrn,
                        "fields", fields,
                        "extraOptions", extraOptions);

        List<Object> args = new ArrayList<>(2);

        args.add(createCredentialsListWithTypeAndVersion(credentialList));

        Map<String, Object> options = new HashMap<>();
        if (fields != null || memberUrn != null) {
            Map<String, String> allFields = new HashMap<>();
            if (fields != null)
                allFields.putAll(fields);
            if (memberUrn != null) {
                assert fields != null;
                if (fields.containsKey("KEY_MEMBER")) {
                    String fieldsKeyMember = fields.get("KEY_MEMBER") + "";
                    if (Objects.equals(fieldsKeyMember, memberUrn.getValue()))
                        LOG.error("Note: you specified both memberUrn and added KEY_MEMBER in fields, for createKey call." +
                                "They are equal, so no problem. Note that the createKey call only requires one of them.");
                    else
                        LOG.error("WARNING: you specified both memberUrn and added KEY_MEMBER in fields for createKey call." +
                                "They are NOT equal! memberUrn='" + memberUrn + "' " +
                                "KEY_MEMBER field value='" + fieldsKeyMember + "' " +
                                "Will give preference to memberUrn! THIS IS MOST LIKELY A BUG!");
                }
                allFields.put("KEY_MEMBER", memberUrn.getValue());
            }
            options.put("fields", allFields);
        }
        if (extraOptions != null)
            options.putAll(extraOptions);
        args.add(options);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "createKey", "create_key",
                args, MemberKeyInfo::new);
    }

    @ApiMethod(order = 9, hint = "delete_key call:")
    @Nonnull
    public FederationApiReply<Boolean> deleteKey(@Nonnull SfaConnection con,
                                                 @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                 @Nonnull List<AnyCredential> credentialList,
                                                 @ApiMethodParameter(name = "memberUrn", parameterType = ApiMethodParameterType.USER_URN)
                                                 @Nonnull GeniUrn memberUrn,
                                                 @ApiMethodParameter(name = "keyId", parameterType = ApiMethodParameterType.STRING)
                                                 @Nonnull String keyId,
                                                 @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                 @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "memberUrn", memberUrn,
                        "keyId", keyId,
                        "extraOptions", extraOptions);

        List<Object> args = new ArrayList<>(3);

        args.add(memberUrn.toString());
        args.add(keyId);

        args.add(createCredentialsListWithTypeAndVersion(credentialList));
        args.add(createOptionsMap(extraOptions));

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "deleteKey", "delete_key",
                args, BOOLEAN_REPLY_CONVERTER, false);

    }

    @ApiMethod(order = 10, hint = "update_key call: Update the details of a key pair for given member ")
    @Nonnull
    public FederationApiReply<String> updateKey(@Nonnull SfaConnection con,
                                                @ApiMethodParameter(name = "credentialList", parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                @Nonnull List<AnyCredential> credentialList,
                                                @ApiMethodParameter(name = "memberUrn", parameterType = ApiMethodParameterType.USER_URN)
                                                @Nonnull GeniUrn memberUrn,
                                                @ApiMethodParameter(name = "keyId", parameterType = ApiMethodParameterType.STRING)
                                                @Nonnull String keyId,
                                                @ApiMethodParameter(name = "fields", parameterType = ApiMethodParameterType.CH_API1_FIELDS)
                                                @Nonnull Map<String, String> fields,
                                                @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("credentialList", credentialList,
                "memberUrn", memberUrn,
                "keyId", keyId,
                "fields", fields,
                "extraOptions", extraOptions);
        List<Object> memberExtraArgument = new ArrayList<>();
        memberExtraArgument.add(memberUrn.getValue());
        return genericUpdateCall(methodParams, con, "updateKey", "update_key", credentialList, keyId, null, fields, extraOptions, memberExtraArgument);
    }
}
