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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * FederationRegistryApi1
 */
public class FederationRegistryApi1 extends AbstractFederationApi1 {
    /**
     * A human readable name for the implemented API
     *
     * @return "Uniform Federation Registry API v1";
     */
    static public String getApiName() {
        return "Uniform Federation Registry API v1";
    }

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

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

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

    @ApiMethod(order = 1, hint = "get_version call: Provide a structure detailing the version information as well as details of accepted options 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));
    }

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

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

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

    private static final List<GetVersionResult.FieldInfo> MINIMUM_SERVICE_FIELDS =
            Collections.unmodifiableList(Arrays.asList(
                    new GetVersionResult.FieldInfo("SERVICE_URN", GetVersionResult.FieldInfo.FieldType.URN),
                    new GetVersionResult.FieldInfo("SERVICE_URL", GetVersionResult.FieldInfo.FieldType.URL),
                    new GetVersionResult.FieldInfo("SERVICE_CERT", GetVersionResult.FieldInfo.FieldType.CERTIFICATE),
                    new GetVersionResult.FieldInfo("SERVICE_NAME", GetVersionResult.FieldInfo.FieldType.STRING),
                    new GetVersionResult.FieldInfo("SERVICE_DESCRIPTION", GetVersionResult.FieldInfo.FieldType.STRING)
            ));

    @Nonnull
    @Override
    public List<GetVersionResult.FieldInfo> getMinimumFields(String objectName) {
        if (objectName.equalsIgnoreCase("SERVICE")) {
            return MINIMUM_SERVICE_FIELDS;
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    @Nullable
    public String getMethodObject(Method method) {
        if (Objects.equals(method.getName(), "lookupAggregates")
                || Objects.equals(method.getName(), "lookupMemberAuthorities")
                || Objects.equals(method.getName(), "lookupSliceAuthorities"))
            return "SERVICE";

        return null;
    }

    public class ServiceDetails {
        private final ApiInfo.ApiName serverRole;
        private GeniUrn urn;
        private URL url;
        private String certificate;
        private String name;
        private String description;

        private final Map<String, Object> extraFields = new HashMap<>();

        public ServiceDetails(ApiInfo.ApiName serverRole, Map fields) throws BadReplyGeniException {
            this.serverRole = serverRole;
            for (Object entryO : fields.entrySet()) {
                Map.Entry entry = (Map.Entry) entryO;
                assert entry.getKey() instanceof String : "not String in in ServiceDetails fields=" + fields;
                String key = (String) entry.getKey();

                Object value = entry.getValue();
                String stringValue = null;
                if (value instanceof String)
                    stringValue = (String) value;

                boolean known = false;

                if (Objects.equals(key, "SERVICE_URN")) {
                    assert stringValue != null : "service value must be String for key SERVICE_URN";
                    urn = GeniUrn.parse(stringValue);
                    known = true;
                }
                if (Objects.equals(key, "SERVICE_URL")) {
                    try {
                        assert stringValue != null : "service value must be String for key SERVICE_URL";
                        url = new URL(stringValue);
                    } catch (MalformedURLException e) {
                        if (!handleMalformedReplies)
                            throw new BadReplyGeniException("Error parsing Federation Registry reply. Malformed URL \"" + value + "\". Error: " + e.getMessage(), e);
                        else {
                            e.printStackTrace();
                            url = null;
                        }
                    }
                    known = true;
                }
                if (Objects.equals(key, "SERVICE_CERTIFICATE")) {
                    assert stringValue != null : "service value must be String for key SERVICE_CERTIFICATE";
                    certificate = stringValue;
                    known = true;
                }
                if (Objects.equals(key, "SERVICE_NAME")) {
                    assert stringValue != null : "service value must be String for key SERVICE_NAME";
                    name = stringValue;
                    known = true;
                }
                if (Objects.equals(key, "SERVICE_DESCRIPTION")) {
                    assert stringValue != null : "service value must be String for key SERVICE_DESCRIPTION";
                    description = stringValue;
                    known = true;
                }
                if (!known)
                    extraFields.put(key, value);
            }

            //not true, could exclude these if fields argument doesn't include them
//            if (urn == null || url == null) {
//                if (!handleMalformedReplies) {
//                    throw new JFedException("Error processing ServiceDetails. Must contain keys SERVICE_URN and SERVICE_URL, but contained only: "+fields.keySet());
//                }
//                else {
//                    LOG.error("Error processing ServiceDetails. Must contain keys SERVICE_URN and SERVICE_URL, but contained only: "+fields.keySet());
//                }
//            }

        }

        public ApiInfo.ApiName getServerRole() {
            return serverRole;
        }

        public GeniUrn getUrn() {
            return urn;
        }

        public URL getUrl() {
            return url;
        }

        public String getCertificate() {
            return certificate;
        }

        public String getName() {
            return name;
        }

        public String getDescription() {
            return description;
        }

        @Override
        public String toString() {
            return "ServiceDetails{" +
                    "serverRole=" + serverRole +
                    ", urn=" + urn +
                    ", url=" + url +
                    ", certificate='" + certificate + '\'' +
                    ", name='" + name + '\'' +
                    ", description='" + description + '\'' +
                    ", extraFields=" + extraFields +
                    '}';
        }
    }

    private FederationApiReply<List<ServiceDetails>> genericLookupServiceDetails(@Nullable Map<String, Object> methodParams,
                                                                                 @Nonnull SfaConnection con,
                                                                                 @Nonnull String javaMethodName,
                                                                                 @Nonnull String apiMethodName,
                                                                                 @Nonnull ApiInfo.ApiName role,
                                                                                 @Nullable List<String> filter,
                                                                                 @Nullable Map<String, ?> match,
                                                                                 @Nullable Map<String, Object> extraOptions) throws JFedException {

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

        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, javaMethodName, apiMethodName,
                args, resultValueObject -> {
                    List<Map> resultList = apiSpecifiesListOfT(Map.class, resultValueObject);
                    List<ServiceDetails> resList = new ArrayList<>();
                    for (Map h : resultList) {
                        ServiceDetails serviceDetails = new ServiceDetails(role, h);
                        resList.add(serviceDetails);
                    }
                    return resList;
                });
    }

    @ApiMethod(order = 2, hint = "lookup_aggregates call: Return information about all aggregates associated with the Federation", unprotected = true)
    @Nonnull
    public FederationApiReply<List<ServiceDetails>> lookupAggregates(@Nonnull SfaConnection con,
                                                                     @ApiMethodParameter(name = "filter", hint = "fields names included in reply. If omitted: all fields.",
                                                                             required = false, parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                     @Nullable List<String> filter,
                                                                     @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                     @Nullable Map<String, ?> match,
                                                                     @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("filter", filter,
                "match", match,
                "extraOptions", extraOptions);
        return genericLookupServiceDetails(methodParams, con, "lookupAggregates", "lookup_aggregates",
                ApiInfo.ApiName.GENI_AM, filter, match, extraOptions);
    }

    @ApiMethod(order = 3, hint = "lookup_member_authorities call: Return information about all MA’s associated with the Federation", unprotected = true)
    @Nonnull
    public FederationApiReply<List<ServiceDetails>> lookupMemberAuthorities(@Nonnull SfaConnection con,
                                                                            @ApiMethodParameter(name = "filter", hint = "fields names included in reply. If omitted: all fields.", required = false, parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                            @Nullable List<String> filter,
                                                                            @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                            @Nullable Map<String, ?> match,
                                                                            @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("filter", filter,
                "match", match,
                "extraOptions", extraOptions);
        return genericLookupServiceDetails(methodParams, con, "lookupMemberAuthorities", "lookup_member_authorities",
                ApiInfo.ApiName.GENI_CH_MA, filter, match, extraOptions);
    }

    @ApiMethod(order = 4, hint = "lookup_slice_authorities call: Return information about all SA’s associated with the Federation", unprotected = true)
    @Nonnull
    public FederationApiReply<List<ServiceDetails>> lookupSliceAuthorities(@Nonnull SfaConnection con,
                                                                           @ApiMethodParameter(name = "filter", hint = "fields names included in reply. If omitted: all fields.", required = false, parameterType = ApiMethodParameterType.CH_API1_FILTER)
                                                                           @Nullable List<String> filter,
                                                                           @ApiMethodParameter(name = "match", parameterType = ApiMethodParameterType.CH_API1_MATCH)
                                                                           @Nullable Map<String, ?> match,
                                                                           @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("filter", filter,
                "match", match,
                "extraOptions", extraOptions);
        return genericLookupServiceDetails(methodParams, con, "lookupSliceAuthorities", "lookup_slice_authorities",
                ApiInfo.ApiName.GENI_CH_SA, filter, match, extraOptions);
    }

    @ApiMethod(order = 5, hint = "lookup_authorities_for_urns call: Lookup the authorities for a given URNs", unprotected = true)
    @Nonnull
    public FederationApiReply<Map<GeniUrn, URL>> lookupAuthoritiesForUrns(@Nonnull SfaConnection con,
                                                                          @ApiMethodParameter(name = "urns", hint = "URNs of entities for which the authority is requested", parameterType = ApiMethodParameterType.LIST_OF_URN_STRING)
                                                                          @Nonnull List<GeniUrn> urns) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("urns", urns);

        List<Object> args = new ArrayList<>(1);
        final List<String> stringUrns =
                urns.stream().map(GeniUrn::toString).collect(Collectors.toList());
        args.add(stringUrns);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "lookupAuthoritiesForUrns", "lookup_authorities_for_urns",
                args, FederationRegistryApi1::processAuthoritiesLookup);
    }

    static Map<GeniUrn, URL> processAuthoritiesLookup(Object resultValueObject) throws BadReplyGeniException {
        Map<String, String> resMap = apiSpecifiesMapStringToString(resultValueObject);
        Map<GeniUrn, URL> urls = new HashMap<>();

        for (Map.Entry<String, String> e : resMap.entrySet()) {
            GeniUrn urn;
            try {
                urn = new GeniUrn(e.getKey());
            } catch (GeniUrn.GeniUrnParseException ex) {
                throw new BadReplyGeniException("Error processing lookup_authorities_for_urns result, not an urn: \"" + e.getKey() + "\": " +
                        " error message: \"" + ex.getMessage() + "\"", ex);
            }

            URL url;
            try {
                url = new URL(e.getValue());
            } catch (MalformedURLException ex) {
                throw new BadReplyGeniException("Error processing lookup_authorities_for_urns result, not an url: \"" + e.getValue() + "\": " +
                        " error message: \"" + ex.getMessage() + "\"", ex);
            }

            urls.put(urn, url);
        }

        return urls;
    }


    @ApiMethod(order = 6, hint = "get_trust_roots call: Return list of trust roots (certificates) associated with this CH. " +
            "Often this concatenates of the trust roots of the included authorities.", unprotected = true)
    @Nonnull
    public FederationApiReply<List<String>> getTrustRoots(@Nonnull SfaConnection con) throws JFedException {
        List<Object> args = new ArrayList<>();

        return executeAndLogXmlRpcCommandGeni(null, con, "getTrustRoots", "get_trust_roots",
                args, AbstractApi::apiSpecifiesListOfString);
    }
}
