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.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

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

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

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

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

    public FederationRegistryApi2(@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, true));
    }

    @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", "SERVICE_URN", GetVersionResult.FieldInfo.FieldType.URN, true/*match*/, null/*protect*/), //required=true
                    new GetVersionResult.FieldInfo("SERVICE", "SERVICE_URL", GetVersionResult.FieldInfo.FieldType.URL, true/*match*/, null/*protect*/), //required=true
                    new GetVersionResult.FieldInfo("SERVICE", "SERVICE_TYPE", GetVersionResult.FieldInfo.FieldType.STRING, true/*match*/, null/*protect*/), //required=true
                    new GetVersionResult.FieldInfo("SERVICE", "SERVICE_CERT", GetVersionResult.FieldInfo.FieldType.CERTIFICATE, false/*match*/, null/*protect*/), //required=false
                    new GetVersionResult.FieldInfo("SERVICE", "SERVICE_NAME", GetVersionResult.FieldInfo.FieldType.STRING, false/*match*/, null/*protect*/), //required=true
                    new GetVersionResult.FieldInfo("SERVICE", "SERVICE_DESCRIPTION", GetVersionResult.FieldInfo.FieldType.STRING, false/*match*/, null/*protect*/), //required=false
                    new GetVersionResult.FieldInfo("SERVICE", "SERVICE_PEERS", GetVersionResult.FieldInfo.FieldType.PEERS, false/*match*/, null/*protect*/) //required=false
            ));

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

    public static class Peer {
        private final String version;
        private final String urlString;
        private final URL url;

        public Peer(Map h) {
            version = (String) h.get("version");
            urlString = (String) h.get("url");

            URL url = null;
            if (urlString != null) {
                try {
                    url = new URL(urlString);
                } catch (MalformedURLException e) {
                    LOG.warn("Malformed url: \"" + urlString + "\"", e);
                }
            }
            this.url = url;
        }

        public String getVersion() {
            return version;
        }

        public URL getUrl() {
            return url;
        }

        @Override
        public String toString() {
            return "Peer{" +
                    "version='" + version + '\'' +
                    ", urlString='" + urlString + '\'' +
                    ", url=" + url +
                    '}';
        }
    }

    public static class ServiceDetails {
        private String serverRoleString;
        private ApiInfo.ApiName serverRole;
        private GeniUrn urn;
        private URL url;
        private String certificate;
        private String name;
        private String description;
        private List<Peer> peers;

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

        public ServiceDetails(Object resultValueObject, boolean failOnMalformedReplies) throws BadReplyGeniException {
            Map<String, Object> fields = apiSpecifiesMapStringToObject(resultValueObject);
            for (Map.Entry<String,Object> entry : fields.entrySet()) {
                boolean known = false;

                if (Objects.equals(entry.getKey(), "SERVICE_URN")) {
                    try {
                        urn = new GeniUrn(apiSpecifiesNonNullString(entry.getValue()));
                    } catch (BadReplyGeniException e) {
                        failOrLog("service value must be String for key SERVICE_URN", e, failOnMalformedReplies);
                    } catch (GeniUrn.GeniUrnParseException e) {
                        failOrLog("Could not parse the value of SERVICE_URN", e, failOnMalformedReplies);
                    }
                    known = true;
                }
                if (Objects.equals(entry.getKey(), "SERVICE_TYPE")) {
                    try {
                        this.serverRoleString = apiSpecifiesNonNullString(entry.getValue());
                        this.serverRole = null;
                        if (serverRoleString.equalsIgnoreCase("MEMBER_AUTHORITY"))
                            this.serverRole = ApiInfo.ApiName.GENI_CH_MA;
                        if (serverRoleString.equalsIgnoreCase("SLICE_AUTHORITY"))
                            this.serverRole = ApiInfo.ApiName.GENI_CH_SA;
                        if (serverRoleString.equalsIgnoreCase("AGGREGATE_MANAGER"))
                            this.serverRole = ApiInfo.ApiName.GENI_AM;
                        if (serverRoleString.equalsIgnoreCase("STITCHING_COMPUTATION_SERVICE"))
                            this.serverRole = ApiInfo.ApiName.GENI_SCS;
                        // also in API:   CREDENTIAL_STORE and LOGGING_SERVICE
                    } catch (BadReplyGeniException e) {
                        failOrLog("service value must be String for key SERVICE_TYPE", e, failOnMalformedReplies);
                    }
                    known = true;
                }
                if (Objects.equals(entry.getKey(), "SERVICE_URL")) {
                    try {
                        url = new URL(apiSpecifiesNonNullString(entry.getValue()));
                    } catch (BadReplyGeniException e) {
                        failOrLog("service value must be String for key SERVICE_URL", e, failOnMalformedReplies);
                    } catch (MalformedURLException e) {
                        failOrLog("Error parsing Federation Registry reply. Malformed URL \"" + entry.getValue() + "\". Error: " + e.getMessage(), e, failOnMalformedReplies);
                    }
                    known = true;
                }
                if (Objects.equals(entry.getKey(), "SERVICE_CERTIFICATE")) {
                    try {
                        certificate = apiSpecifiesNonNullString(entry.getValue());
                    } catch (BadReplyGeniException e) {
                        failOrLog("service value must be String for key SERVICE_CERTIFICATE", e, failOnMalformedReplies);
                    }
                    known = true;
                }
                if (Objects.equals(entry.getKey(), "SERVICE_NAME")) {
                    try {
                        name = apiSpecifiesNonNullString(entry.getValue());
                    } catch (BadReplyGeniException e) {
                        failOrLog("service value must be String for key SERVICE_NAME", e, failOnMalformedReplies);
                    }
                    known = true;
                }
                if (Objects.equals(entry.getKey(), "SERVICE_DESCRIPTION")) {
                    try {
                        description = apiSpecifiesNonNullString(entry.getValue());
                    } catch (BadReplyGeniException e) {
                        failOrLog("service value must be String for key SERVICE_DESCRIPTION", e, failOnMalformedReplies);
                    }
                    known = true;
                }
                if (Objects.equals(entry.getKey(), "SERVICE_PEERS")) {
                    known = true;
                    try {
                        List<Map> peersList = apiSpecifiesListOfT(Map.class, entry.getValue());

                        peers = new ArrayList<>();
                        for (Map ht : peersList) {
                            peers.add(new Peer(ht));
                        }
                    } catch (BadReplyGeniException e) {
                        failOrLog("SERVICE_PEERS must contain a list of Maps", e, failOnMalformedReplies);
                    }
                }
                if (!known)
                    extraFields.put(entry.getKey(), entry.getValue());
            }

            //below is not correct: urn ==null or url == null IS allowed: for example, they are excluded these when the 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());
//                }
//            }

        }

        private static void failOrLog(String message, Exception e, boolean failOnMalformedReplies) throws BadReplyGeniException {
            if (failOnMalformedReplies) {
                throw new BadReplyGeniException(message, e);
            } else {
                LOG.info(message, e);
            }
        }

        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 +
                    ", type=" + serverRoleString +
                    ", urn=" + urn +
                    ", url=" + url +
                    ", certificate='" + certificate + '\'' +
                    ", name='" + name + '\'' +
                    ", description='" + description + '\'' +
                    ", peers='" + peers + '\'' +
                    ", extraFields=" + extraFields +
                    '}';
        }
    }

    private static class LookupResultServiceDetailsConverter implements LookupResultConverter<ServiceDetails> {
        private final boolean handleMalformedReplies;

        public LookupResultServiceDetailsConverter(boolean handleMalformedReplies) {
            this.handleMalformedReplies = handleMalformedReplies;
        }

        @Override
        public ServiceDetails convert(Map h) throws BadReplyGeniException {
            return new ServiceDetails(h, !handleMalformedReplies);
        }
    }

    @ApiMethod(order = 2, hint = "lookup call: Return information about a service associated with the Federation", unprotected = true)
    @Nonnull
    public FederationApiReply<LookupResult<ServiceDetails>> lookup(@Nonnull SfaConnection con,
                                                                   @ApiMethodParameter(name = "type", hint = "object type to lookup",
                                                                           guiDefault = "SERVICE", parameterType = ApiMethodParameterType.CH_API2_OBJECT_TYPE)
                                                                   @Nonnull String type,
                                                                   @ApiMethodParameter(name = "filter", required = false, hint = "fields names included in reply. If omitted: all fields.",
                                                                           parameterType = ApiMethodParameterType.CH_API2_FILTER)
                                                                   @Nullable List<String> filter,
                                                                   @ApiMethodParameter(name = "match",
                                                                           required = false,
                                                                           parameterType = ApiMethodParameterType.CH_API2_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 genericLookup(methodParams, con, "lookup",
                type,
                null, match, filter, extraOptions, null, new LookupResultServiceDetailsConverter(handleMalformedReplies)
                /* credentials */
                /* extra arguments*/);
    }


    @ApiMethod(order = 4, hint = "convenience method which calls \"lookup\" with SERVICE_TYPE=\"AGGREGATE_MANAGER\"", unprotected = true, convenienceMethodFor = "lookup", convenienceMethodObjectType = "SERVICE")
    @Nonnull
    public FederationApiReply<LookupResult<ServiceDetails>> lookupAM(@Nonnull SfaConnection con,
                                                                     @ApiMethodParameter(name = "filter",
                                                                             hint = "fields names included in reply. If omitted: all fields.",
                                                                             required = false,
                                                                             parameterType = ApiMethodParameterType.CH_API2_FILTER)
                                                                     @Nullable List<String> filter,
                                                                     @ApiMethodParameter(name = "match",
                                                                             required = false,
                                                                             parameterType = ApiMethodParameterType.CH_API2_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 {
        if (match == null) {
            match = new HashMap<String, String>();
        }
        if (!match.containsKey("SERVICE_TYPE")) {
            match = Collections.singletonMap("SERVICE_TYPE", "AGGREGATE_MANAGER");
        }

        return lookup(con, "SERVICE", filter, match, extraOptions);
    }

    @ApiMethod(order = 4, hint = "convenience method which calls \"lookup\" with SERVICE_TYPE=\"SLICE_AUTHORITY\"", unprotected = true, convenienceMethodFor = "lookup", convenienceMethodObjectType = "SERVICE")
    @Nonnull
    public FederationApiReply<LookupResult<ServiceDetails>> lookupSA(@Nonnull SfaConnection con,
                                                                     @ApiMethodParameter(name = "filter",
                                                                             hint = "fields names included in reply. If omitted: all fields.",
                                                                             required = false,
                                                                             parameterType = ApiMethodParameterType.CH_API2_FILTER)
                                                                     @Nullable List<String> filter,
                                                                     @ApiMethodParameter(name = "match",
                                                                             required = false,
                                                                             parameterType = ApiMethodParameterType.CH_API2_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 {
        if (match == null) {
            match = new HashMap<>();
        }
        if (!match.containsKey("SERVICE_TYPE")) {
            Map<String, Object> helper = new HashMap<>();
            helper.putAll(match);
            helper.put("SERVICE_TYPE", "SLICE_AUTHORITY");
            match = helper;
        }

        return lookup(con, "SERVICE", filter, match, extraOptions);
    }

    @ApiMethod(order = 5, hint = "convenience method which calls \"lookup\" with SERVICE_TYPE=\"MEMBER_AUTHORITY\"", unprotected = true, convenienceMethodFor = "lookup", convenienceMethodObjectType = "SERVICE")
    @Nonnull
    public FederationApiReply<LookupResult<ServiceDetails>> lookupMA(@Nonnull SfaConnection con,
                                                                     @ApiMethodParameter(name = "filter",
                                                                             hint = "fields names included in reply. If omitted: all fields.",
                                                                             required = false,
                                                                             parameterType = ApiMethodParameterType.CH_API2_FILTER)
                                                                     @Nullable List<String> filter,
                                                                     @ApiMethodParameter(name = "match",
                                                                             required = false,
                                                                             parameterType = ApiMethodParameterType.CH_API2_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 {
        if (match == null) {
            match = new HashMap<String, String>();
        }
        if (!match.containsKey("SERVICE_TYPE")) {
            Map<String, Object> helper = new HashMap<>();
            helper.putAll(match);
            helper.put("SERVICE_TYPE", "MEMBER_AUTHORITY");
            match = helper;
        }

        return lookup(con, "SERVICE", filter, match, extraOptions);
    }

    @ApiMethod(order = 10, 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);
        List stringUrns = urns.stream().map(GeniUrn::toString).collect(Collectors.toList());
        args.add(stringUrns);

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

    @ApiMethod(order = 20, 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);
    }
}
