package be.iminds.ilabt.jfed.lowlevel;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Service;
import be.iminds.ilabt.jfed.lowlevel.api.*;
import be.iminds.ilabt.jfed.lowlevel.connection.ConnectionConfig;
import be.iminds.ilabt.jfed.lowlevel.fedmon.Fedmon;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * ApiInfo contains info on the APIs that jFed can communicate with
 *
 * This information includes:
 *   - Which connection is needed by each API
 *   - Which String representation is used for this api in Service.api
 *   (- Which class jFed uses for this API) TODO: needed here?
 *
 * Information not found here:
 *   - Which API runs in which Testbed/Server/Service/URL. See TestbedInfoSource for this.
 */
public class ApiInfo {
    private static final Logger LOG = LoggerFactory.getLogger(ApiInfo.class);


    public static class Api {
        private final ApiName name;
        private final String version;

        public Api(@Nonnull ApiName name, @Nonnull String version) {
            this.name = name;
            this.version = version;
        }
        public Api(@Nonnull ApiName name, int version) {
            this.name = name;
            this.version = version+"";
        }
        public Api(@Nonnull String id, @Nonnull String version) {
            this.name = findById(id);
            if (name == null)
                throw new IllegalArgumentException("No API found with id \""+id+"\"");
            this.version = version;
        }

        public String getId() {
            return name.getId();
        }

        public ApiName getName() {
            return name;
        }

        public String getVersion() {
            return version;
        }

        public boolean matches(Service service) {
            return Objects.equals(normalizeId(service.getApi()), normalizeId(getId()))
                    && Objects.equals(version.trim(), service.getApiVersion().trim());
        }

        public boolean matchesId(String id) {
            return Objects.equals(normalizeId(getId()), normalizeId(id));
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Api api = (Api) o;

            if (name != api.name) return false;
            if (!Objects.equals(version, api.version)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = name.hashCode();
            result = 31 * result + version.hashCode();
            return result;
        }
    }


    public enum ApiName {
        PLANETLAB_SLICE_REGISTRY("PlanetLab.SliceRegistry"),
        PROTOGENI_SA("ProtoGeni.SA"),
        PROTOGENI_CH("ProtoGeni.CH"),
        GENI_AM("Geni.AM"),
        GENI_CH("Geni.CH"),
        GENI_CH_SA("Geni.SA"),
        GENI_CH_MA("Geni.MA"),
        GENI_SCS("Geni.SCS"),
        GTS_API("GTS.API", ConnectionConfig.Authentication.HTTP_BASIC, ConnectionConfig.Type.HTTP),
        FED4FIRE_FEDMON("Fed4Fire.Fedmon", ConnectionConfig.Authentication.NONE, ConnectionConfig.Type.HTTP),
        FED4FIRE_RESERVATION_CALENDER_PAGE("Fed4Fire.ReservationCalendarPage", ConnectionConfig.Authentication.NONE, ConnectionConfig.Type.HTTP),
        FED4FIRE_SLA_COLLECTOR("Fed4Fire.SlaCollector", ConnectionConfig.Authentication.HTTP_BASIC, ConnectionConfig.Type.HTTP),
        FED4FIRE_REPUTATION_SERVICE("Fed4Fire.ReputationService", ConnectionConfig.Authentication.NONE, ConnectionConfig.Type.HTTP);

        private final String id;
        private final ConnectionConfig.Authentication authentication;
        private final ConnectionConfig.Type connectionType;
        //protocol (HTTP or HTTPS) does not depend on API, but on URL. Some APIs will work over both http and https.


        ApiName(@Nonnull String id, @Nonnull ConnectionConfig.Authentication authentication, @Nonnull ConnectionConfig.Type connectionType) {
            this.id = id;
            this.authentication = authentication;
            this.connectionType = connectionType;
        }

        ApiName(@Nonnull String id) {
            this.id = id;
            this.authentication = ConnectionConfig.Authentication.SSL_CLIENT_AUTH;
            this.connectionType = ConnectionConfig.Type.SFA;
        }

        public String getId() {
            return id;
        }

        String getNormalizedId() {
            return normalizeId(id);
        }

        public boolean matchesId(String id) {
            return Objects.equals(normalizeId(this.id), normalizeId(id));
        }

        public ConnectionConfig.Authentication getAuthentication() {
            return authentication;
        }

        public ConnectionConfig.Type getConnectionType() {
            return connectionType;
        }

    }

    private static final Map<String, ApiName> apiByExactId;
    private static final Map<String, ApiName> apiByForgivingId;

    static {
        apiByExactId = new HashMap<>();
        apiByForgivingId = new HashMap<>();
        for (ApiName api : ApiName.values()) {
            apiByExactId.put(api.getId(), api);
            apiByForgivingId.put(api.getNormalizedId(), api);
        }

        //add some alternative IDs that might be used
        apiByForgivingId.put("am", ApiName.GENI_AM);
        apiByForgivingId.put("scs", ApiName.GENI_SCS);
        apiByForgivingId.put("reputationservice", ApiName.FED4FIRE_REPUTATION_SERVICE);
        apiByForgivingId.put("reservationcalendarpage", ApiName.FED4FIRE_RESERVATION_CALENDER_PAGE);
        apiByForgivingId.put("slacollector", ApiName.FED4FIRE_SLA_COLLECTOR);
        apiByForgivingId.put("gts", ApiName.GTS_API);
        apiByForgivingId.put("planetlabsr", ApiName.PLANETLAB_SLICE_REGISTRY);
    }

    private static String normalizeId(String id) {
        return id.toLowerCase().replaceAll("[^A-Za-z0-9]", "");
    }

    public static ApiName getByExactId(String id) {
        return apiByExactId.get(id);
    }

    public static ApiName findById(String id) {
        assert id != null;
        assert !id.isEmpty();
        String normalizedId = normalizeId(id);
        assert !normalizedId.isEmpty();
        return apiByForgivingId.get(normalizedId);
    }


    public static Api classToApi(Class targetClass) {
        if (Objects.equals(targetClass, AggregateManager3.class))
            return new Api(ApiName.GENI_AM, 3);
        if (Objects.equals(targetClass, AggregateManager2.class))
            return new Api(ApiName.GENI_AM, 2);
        if (Objects.equals(targetClass, ProtogeniSliceAuthority.class))
            return new Api(ApiName.PROTOGENI_SA, 1);
        if (Objects.equals(targetClass, ProtoGeniClearingHouse1.class))
            return new Api(ApiName.PROTOGENI_CH, 1);
        if (Objects.equals(targetClass, PlanetlabSfaRegistryInterface.class))
            return new Api(ApiName.PLANETLAB_SLICE_REGISTRY, 1);
        if (Objects.equals(targetClass, StitchingComputationService.class))
            return new Api(ApiName.GENI_SCS, 1);
        if (Objects.equals(targetClass, Fedmon.class))
            return new Api(ApiName.FED4FIRE_FEDMON, 1);
        if (Objects.equals(targetClass, FederationRegistryApi1.class))
            return new Api(ApiName.GENI_CH, 1);
        if (Objects.equals(targetClass, FederationMemberAuthorityApi1.class))
            return new Api(ApiName.GENI_CH_MA, 1);
        if (Objects.equals(targetClass, FederationSliceAuthorityApi1.class))
            return new Api(ApiName.GENI_CH_SA, 1);
        if (Objects.equals(targetClass, FederationRegistryApi2.class))
            return new Api(ApiName.GENI_CH, 2);
        if (Objects.equals(targetClass, FederationMemberAuthorityApi2.class))
            return new Api(ApiName.GENI_CH_MA, 2);
        if (Objects.equals(targetClass, FederationSliceAuthorityApi2.class))
            return new Api(ApiName.GENI_CH_SA, 2);
        if (Objects.equals(targetClass, GtsRest.class))
            return new Api(ApiName.GTS_API, 1);
        if (Objects.equals(targetClass, SlaCollector.class))
            return new Api(ApiName.FED4FIRE_SLA_COLLECTOR, 1);
        return null;
    }



    public static String findUrlString(Server server, ApiName apiName, int version) {
        assert apiName != null;
        return findUrlString(server, new Api(apiName, version));
    }
    public static String findUrlString(Server server, ApiName apiName, String version) {
        assert apiName != null;
        assert version != null;
        return findUrlString(server, new Api(apiName, version));
    }
    public static String findUrlString(Server server, Api api) {
        assert server != null;
        Service service = findService(server, api);
        if (service == null)
            return null;
        return service.getUrl();
    }


    public static URL findUrl(Server server, ApiName apiName, int version) {
        assert apiName != null;
        return findUrl(server, new Api(apiName, version));
    }
    public static URL findUrl(Server server, ApiName apiName, String version) {
        assert apiName != null;
        assert version != null;
        return findUrl(server, new Api(apiName, version));
    }
    public static URL findUrl(Server server, Api api) {
        assert server != null;
        String urlString = findUrlString(server, api);
        if (urlString == null)
            return null;
        try {
            return new URL(urlString);
        } catch (MalformedURLException e) {
            LOG.error("Invalid URL in Testbed config. server.id="+server.getId()+" url=\""+urlString+"\"", e);
            return null;
        }
    }


    public static Service findService(Server server, ApiName apiName, int version) {
        assert apiName != null;
        return findService(server, new Api(apiName, version));
    }
    public static Service findService(Server server, ApiName apiName, String version) {
        assert apiName != null;
        assert version != null;
        return findService(server, new Api(apiName, version));
    }
    public static Service findService(Server server, Api api) {
        assert server != null;

        if (server.getServices() == null || server.getServices().isEmpty())
            return null;

        for (Service service : server.getServices()) {
            if (api.matches(service))
                return service;
        }
        return null;
    }

    public static boolean hasUrl(Server server, URL url) {
        return hasUrl(server, url.toExternalForm());
    }
    public static boolean hasUrl(Server server, String urlString) {
        for (Service service : server.getServices()) {
            if (Objects.equals(urlString, service.getUrl()))
                return true;
        }
        return false;
    }
    public static boolean hasUrl(Service service, URL url) {
        return Objects.equals(url.toExternalForm(), service.getUrl());
    }
    public static boolean hasUrl(Service service, String urlString) {
        return Objects.equals(urlString, service.getUrl());
    }

    public static boolean hasService(Server server, ApiName apiName, int version) {
        assert apiName != null;
        return hasService(server, new Api(apiName, version));
    }
    public static boolean hasService(Server server, ApiName apiName, String version) {
        assert apiName != null;
        assert version != null;
        return hasService(server, new Api(apiName, version));
    }
    public static boolean hasService(Server server, Api api) {
        assert server != null;

        if (server.getServices() == null || server.getServices().isEmpty())
            return false;

        for (Service service : server.getServices()) {
            if (api.matches(service))
                return true;
        }
        return false;
    }

    /** @return true if any version of the API is present */
    public static boolean hasService(Server server, ApiName apiName) {
        assert server != null;

        if (server.getServices() == null || server.getServices().isEmpty())
            return false;

        for (Service service : server.getServices()) {
            if (apiName.matchesId(service.getApi()))
                return true;
        }
        return false;
    }

    public static List<GeniUrn> getDefaultComponentManagerForEach(Server server) {
        List<GeniUrn> res = new ArrayList<>();
        res.add(server.getDefaultComponentManagerAsGeniUrn());
        return res;
    }
    public static List<GeniUrn> getDefaultComponentManagerForEach(Server ... servers) {
        List<GeniUrn> res = new ArrayList<>();
        for (Server auth: servers)
            res.add(auth.getDefaultComponentManagerAsGeniUrn());
        return res;
    }
    public static List<GeniUrn> getDefaultComponentManagerForEach(List<Server> servers) {
        return servers.stream().map(Server::getDefaultComponentManagerAsGeniUrn).collect(Collectors.toList());
    }
}
