package be.iminds.ilabt.jfed.lowlevel;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.stream.Stream;

/**
 * BasicTestbedInfoSource basic implementation
 */
public class BasicTestbedInfoSource extends TestbedInfoSource {
    private static final Logger LOG = LoggerFactory.getLogger(BasicTestbedInfoSource.class);

    private final List<Testbed> testbeds = new ArrayList<>();
    private final List<Server> servers = new ArrayList<>();
    private final List<Service> services = new ArrayList<>();

    private final Map<String, Testbed> testbedsById = new HashMap<>();
    private final Map<Integer, Server> serversById = new HashMap<>();
    private final Map<Integer, Service> servicesById = new HashMap<>();

    private final Map<String, Service> servicesByUrl = new HashMap<>();
    private final Map<String, List<Service>> servicesByApi = new HashMap<>();


    protected BasicTestbedInfoSource(Collection<TestbedBuilder> testbedBuilders,
                                     Class<TestbedBuilder> testbedBuilderClass/*lame trick to work around type erasure*/) {
        initCollectionsFromTestbedBuilders(testbedBuilders);
    }

    protected BasicTestbedInfoSource(Collection<Testbed> testbeds) {
        initCollectionsFromTestbeds(testbeds);
    }

    public static BasicTestbedInfoSource createFromTestbeds(Collection<Testbed> testbeds) {
        return new BasicTestbedInfoSource(testbeds);
    }

    public static BasicTestbedInfoSource createFromTestbedBuilders(Collection<TestbedBuilder> testbedBuilders) {
        return new BasicTestbedInfoSource(testbedBuilders, TestbedBuilder.class);
    }

    protected void initCollectionsFromTestbedBuilders(Collection<TestbedBuilder> testbedBuilders) {
        if (testbedBuilders == null)
            return;
        testbedBuilders.forEach(this::initTestbed);
    }

    protected void initCollectionsFromTestbeds(Collection<Testbed> testbeds) {
        if (testbeds == null)
            return;
        testbeds.forEach(this::initTestbed);
    }

    protected static class InitHelper {
        public final List<String> errors = new ArrayList<>();
        public boolean ok = true;

        public void test(boolean ok, String error) {
            if (!ok) {
                this.ok = false;
                errors.add(error);
            }
        }
    }

    protected void initTestbed(Testbed testbed) {
        initTestbed(new TestbedBuilder(testbed, true));
    }

    protected void initTestbed(TestbedBuilder testbedBuilder) {
        InitHelper helper = new InitHelper();
        helper.test(testbedBuilder != null, "testbed == null");
        helper.test(testbedBuilder.getDefaultComponentManagerUrn() != null, "testbed.getDefaultComponentManagerUrn() == null");
        helper.test(GeniUrn.parse(testbedBuilder.getDefaultComponentManagerUrn()) != null, "GeniUrn.parse(testbed.getDefaultComponentManagerUrn()) == null");
        helper.test(testbedBuilder.getId() != null, "testbed.getId() == null");
        helper.test(testbedBuilder.getLongName() != null, "testbed.getLongName() == null");
        assert testbedBuilder.getServers() == null :
                "Testbed should use ServerBuilders. testbedId=" + testbedBuilder.getId();
        helper.test(testbedBuilder.getServers() == null, "testbed.getServers() != null");
        helper.test(testbedBuilder.getServerBuilders() != null, "testbed.getServerBuilders() == null");
        if (testbedBuilder.getServerBuilders() != null)
            helper.test(!testbedBuilder.getServerBuilders().isEmpty(), "testbed.getServerBuilders().isEmpty()");

        if (helper.ok) {
            if (testbedBuilder.getServerBuilders() != null) {
                List<ServerBuilder> usedServerBuilders =
                        new ArrayList<>(testbedBuilder.getServerBuilders().size());
                for (ServerBuilder serverBuilder : testbedBuilder.getServerBuilders()) {
                    helper.test(serverBuilder != null, "server == null");
                    helper.test(serverBuilder.getDefaultComponentManagerUrn() != null, "server.getDefaultComponentManagerUrn() == null");
                    if (serverBuilder.getDefaultComponentManagerUrn() != null)
                        helper.test(GeniUrn.parse(serverBuilder.getDefaultComponentManagerUrn()) != null, "GeniUrn.parse(server.getDefaultComponentManagerUrn()) == null");
                    helper.test(serverBuilder.getUrnTld() != null, "server.getUrnTld() == null");
//                helper.test(server.getFlags() != null, "server.getFlags() == null"); //not important, null or empty doesn't matter

                    //no need to check here: TestbedBuilder.create should set this right
//                    helper.test(serverBuilder.getTestbed() != null, "server.getTestbed() == null");
//                    if (serverBuilder.getTestbed() != null)
//                        helper.test(serverBuilder.getTestbed().getId().equals(testbed.getId()), "!server.getTestbed().getId().equals(testbed.getId())");
//                    if (serverBuilder.getTestbed() != null)
//                        helper.test(serverBuilder.getTestbed() == testbed, "server.getTestbed() != testbed");

                    helper.test(serverBuilder.getName() != null, "server.getName() != null");

                    assert serverBuilder.getServices() == null :
                            "ServerBuilder should use ServiceBuilders. serverId=" + serverBuilder.getId();
                    helper.test(serverBuilder.getServices() == null, "serverBuilder.getServices() != null");

                    if (serverBuilder.getServiceBuilders() != null && !serverBuilder.getServiceBuilders().isEmpty()) {
//                        helper.test(serverBuilder.getServiceBuilders() != null, "server.getServiceBuilders() == null");
                        helper.test(serverBuilder.getBaseUrl() != null, "server.getBaseUrl() == null");
                    }

                    if (helper.ok) {
                        if (serverBuilder.getServiceBuilders() != null) {
                            List<ServiceBuilder> usedServiceBuilders = new ArrayList<>();
                            for (ServiceBuilder serviceBuilder : serverBuilder.getServiceBuilders()) {
                                InitHelper serviceHelper = new InitHelper();

                                serviceHelper.test(serviceBuilder.getApi() != null, "service.getApi() == null");
                                serviceHelper.test(serviceBuilder.getApiVersion() != null, "service.getApiVersion() == null");
                                serviceHelper.test(serviceBuilder.getId() != null, "service.getId() == null");

                                //no need to check here: TestbedBuilder.create should set this right
//                                serviceHelper.test(serviceBuilder.getServer() != null, "service.getServer() == null");
//                                if (serviceBuilder.getServer() != null)
//                                    serviceHelper.test(serviceBuilder.getServer().getId().equals(serverBuilder.getId()), "!service.getServer().getId().equals(server.getId())");
//                                if (serviceBuilder.getServer() != null)
//                                    serviceHelper.test(serviceBuilder.getServer() == serverBuilder, "service.getServer() != server");

                                serviceHelper.test(serviceBuilder.getUrn() != null, "service.getUrn() == null");
                                if (serviceBuilder.getUrn() != null)
                                    serviceHelper.test(GeniUrn.parse(serviceBuilder.getUrn()) != null, "GeniUrn.parse(service.getUrn()) == null");
                                serviceHelper.test(serviceBuilder.getUrl() != null, "service.getUrl() == null");
                                try {
                                    URL serviceUrl = new URL(serviceBuilder.getUrl());
                                    serviceHelper.test(serviceUrl != null, "serviceUrl != null");
                                } catch (MalformedURLException e) {
                                    LOG.error("Service " + serviceBuilder.getId() + " has invalid URL", e);
                                    serviceHelper.test(false, "Service " + serviceBuilder.getId() + " has invalid URL \"" + serviceBuilder.getUrl() + "\"");
                                }

                                if (serviceHelper.ok) {
                                    usedServiceBuilders.add(serviceBuilder);
                                } else {
                                    LOG.error("There is something wrong with Service " + serviceBuilder.getId() + " of Server " + serverBuilder.getId() + " of testbed " + testbedBuilder.getId() + " -> ignoring Service! " + serviceHelper.errors);
                                    //not adding to usedServiceBuilders
                                }
                            }
                            serverBuilder.setServiceBuilders(usedServiceBuilders);
                        } else {
                            serverBuilder.setServiceBuilders(Collections.emptyList());
                        }

                        usedServerBuilders.add(serverBuilder);
                    } else {
                        LOG.error("There is something wrong with Server " + serverBuilder.getId() + " of testbed " + testbedBuilder.getId() + " -> ignoring Server! " + helper.errors);
                        //not adding to usedServerBuilders
                    }
                }

                testbedBuilder.setServerBuilders(usedServerBuilders);
            } else {
                testbedBuilder.setServerBuilders(Collections.emptyList());
            }

            Testbed testbed = testbedBuilder.create();

            if (testbed.getServers() != null) {
                for (Server server : testbed.getServers()) {
                    assert server.getTestbed() != null;
                    assert Objects.equals(server.getTestbed().getId(), testbed.getId());
                    assert server.getTestbed() == testbed;

                    servers.add(server);
                    serversById.put(server.getId(), server);
                    if (server.getServices() != null) {
                        for (Service service : server.getServices()) {
                            assert service.getServer() != null;
                            assert Objects.equals(service.getServer().getId(), server.getId());
                            assert service.getServer() == server;

                            services.add(service);
                            servicesById.put(service.getId(), service);

                            Service sameUrlService = servicesByUrl.put(service.getUrl(), service);
                            if (sameUrlService != null) {
                                LOG.warn("2 Services have the same URL: " + service.getId() + " and " + sameUrlService.getId());
                            }
                            List<Service> sameApiServices =
                                    servicesByApi.computeIfAbsent(service.getApi(), k -> new ArrayList<>());
                            sameApiServices.add(service);
                        }
                    }
                }
            }

            testbeds.add(testbed);
            testbedsById.put(testbed.getId(), testbed);
        } else {
            LOG.error("There is something wrong with Testbed " + testbedBuilder.getId() + " -> ignoring Testbed! " + helper.errors);
            //TODO: remove Testbed from Organisation if needed
        }
    }

    @Override
    public List<Server> getServers() {
        return servers;
    }

    @Override
    public List<Testbed> getTestbeds() {
        return testbeds;
    }

    @Override
    public int getServiceCount() {
        return services.size();
    }

    @Override
    public Service getServiceById(Integer id) {
        return servicesById.get(id);
    }

    @Override
    public Server getServerById(Integer id) {
        return serversById.get(id);
    }

    @Override
    public Testbed getTestbedById(String id) {
        return testbedsById.get(id);
    }

    @Override
    public Service getServiceByUrl(String serviceUrlString) {
        return servicesByUrl.get(serviceUrlString);
    }

    @Override
    public Service getServiceByUrl(URL serviceUrl) {
        return servicesByUrl.get(serviceUrl.toExternalForm());
    }


    @Override
    public Service getServiceByUrn(GeniUrn wantedUrn, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred, boolean ignoreUrnTypeAndName, String api, String version) {
        List<Service> servicesWithApi = servicesByApi.get(api);
        List<PairWithUrn<Service>> exactResultSet = new ArrayList<>();
        List<PairWithUrn<Service>> sameTldResultSet = new ArrayList<>();
        List<PairWithUrn<Service>> ignoreTldResultSet = new ArrayList<>();
        for (Service service : servicesWithApi) {
            if (!Objects.equals(version, service.getApiVersion()))
                continue;

            try {
                GeniUrn curUrn = new GeniUrn(service.getUrn());
                if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, ignoreUrnTypeAndName))
                    exactResultSet.add(new PairWithUrn<>(curUrn, service));

                if (matchAllowed == SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY)
                    continue;

                if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_TOPLEVEL, ignoreUrnTypeAndName))
                    sameTldResultSet.add(new PairWithUrn<>(curUrn, service));

                if (matchAllowed == SubAuthMatchAllowed.ALLOW_TOPLEVEL)
                    continue;

                if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_OTHER_SUBAUTHORITY, ignoreUrnTypeAndName))
                    ignoreTldResultSet.add(new PairWithUrn<>(curUrn, service));
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new IllegalStateException("TestbedInfoSource contains a Service with an invalid URN " +
                        "\"" + service.getUrn() + "\" id=" + service.getId(), e);
            }
        }

        return findPreferredResult(wantedUrn, exactResultSet, sameTldResultSet, ignoreTldResultSet, matchPreferred);
    }

    @Override
    public Testbed getTestbedByUrn(GeniUrn wantedUrn, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred, boolean ignoreUrnTypeAndName) {
        List<PairWithUrn<Testbed>> exactResultSet = new ArrayList<>();
        List<PairWithUrn<Testbed>> sameTldResultSet = new ArrayList<>();
        List<PairWithUrn<Testbed>> ignoreTldResultSet = new ArrayList<>();
        for (Testbed testbed : testbeds) {
            try {
                GeniUrn curUrn = new GeniUrn(testbed.getDefaultComponentManagerUrn());
                if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, ignoreUrnTypeAndName))
                    exactResultSet.add(new PairWithUrn<>(curUrn, testbed));

                if (matchAllowed == SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY)
                    continue;

                if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_TOPLEVEL, ignoreUrnTypeAndName))
                    sameTldResultSet.add(new PairWithUrn<>(curUrn, testbed));

                if (matchAllowed == SubAuthMatchAllowed.ALLOW_TOPLEVEL)
                    continue;

                if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_OTHER_SUBAUTHORITY, ignoreUrnTypeAndName))
                    ignoreTldResultSet.add(new PairWithUrn<>(curUrn, testbed));
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new IllegalStateException("TestbedInfoSource contains a Testbed with an invalid URN " +
                        "\"" + testbed.getDefaultComponentManagerUrn() + "\" id=" + testbed.getId(), e);
            }
        }
        return findPreferredResult(wantedUrn, exactResultSet, sameTldResultSet, ignoreTldResultSet, matchPreferred);
    }

    @Override
    public Server getByUrn(GeniUrn wantedUrn, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred, boolean ignoreUrnTypeAndName) {
        assert wantedUrn != null;
        assert matchAllowed != null;
        assert matchPreferred != null;

        List<PairWithUrn<Server>> exactResultSet = new ArrayList<>();
        List<PairWithUrn<Server>> sameTldResultSet = new ArrayList<>();
        List<PairWithUrn<Server>> ignoreTldResultSet = new ArrayList<>();
        for (Server server : servers) {
            Set<String> curUrns = new HashSet<>();
            assert server != null;
            assert server.getDefaultComponentManagerUrn() != null;
            curUrns.add(server.getDefaultComponentManagerUrn());
            if (server.getServices() != null) {
                for (Service service : server.getServices()) {
                    assert service != null;
                    assert service.getUrn() != null :
                            "service " + service.getId() + " of server " + server.getId() + " has urn == null";
                    curUrns.add(service.getUrn());
                }
            }
            for (String curUrnString : curUrns) {
                assert curUrnString != null;
                try {
                    GeniUrn curUrn = new GeniUrn(curUrnString);
                    if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, ignoreUrnTypeAndName))
                        exactResultSet.add(new PairWithUrn<>(curUrn, server));

                    if (matchAllowed == SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY)
                        continue;

                    if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_TOPLEVEL, ignoreUrnTypeAndName))
                        sameTldResultSet.add(new PairWithUrn<>(curUrn, server));

                    if (matchAllowed == SubAuthMatchAllowed.ALLOW_TOPLEVEL)
                        continue;

                    if (matchUrn(curUrn, wantedUrn, SubAuthMatchAllowed.ALLOW_OTHER_SUBAUTHORITY, ignoreUrnTypeAndName))
                        ignoreTldResultSet.add(new PairWithUrn<>(curUrn, server));
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new IllegalStateException("TestbedInfoSource contains a Server with an invalid URN " +
                            "\"" + server.getDefaultComponentManagerUrn() + "\" id=" + server.getId(), e);
                }
            }
        }
        return findPreferredResult(wantedUrn, exactResultSet, sameTldResultSet, ignoreTldResultSet, matchPreferred);
    }


    public static <T> T findPreferredResult(GeniUrn requested, List<PairWithUrn<T>> foundExactMatch, List<PairWithUrn<T>> foundWithTldMatch, List<PairWithUrn<T>> foundIgnoreSubTld, SubAuthMatchPreference preference) {
        assert foundExactMatch != null;
        assert foundWithTldMatch != null;
        assert foundIgnoreSubTld != null;

        switch (preference) {
            case PREFER_EXACT_SUBAUTHORITY: {
                return Stream.concat(foundExactMatch.stream(),
                        Stream.concat(
                                sortBySubAuthDifference(foundWithTldMatch, requested).stream(),
                                sortBySubAuthDifference(foundIgnoreSubTld, requested).stream()))
                        .map(pair -> pair.object)
                        .findFirst().orElse(null);
            }
            case PREFER_TOPLEVEL: {
                return Stream.concat(sortBySubAuthNumber(foundWithTldMatch).stream(),
                        Stream.concat(
                                foundExactMatch.stream(),
                                sortBySubAuthNumber(foundIgnoreSubTld).stream()))
                        .map(pair -> pair.object)
                        .findFirst().orElse(null);
            }
            default:
                throw new RuntimeException("SubAuthMatchPreference " + preference + " no supported");
        }
    }
//    public static <T> List<PairWithUrn<T>> sortByPreference(GeniUrn requested, List<PairWithUrn<T>> found, SubAuthMatchPreference preference) {
//        switch (preference) {
//            case PREFER_EXACT_SUBAUTHORITY: return sortBySubAuthDifference(found, requested);
//            case PREFER_TOPLEVEL: return sortBySubAuthNumber(found);
//            default:
//                throw new RuntimeException("SubAuthMatchPreference "+preference+" no supported");
//        }
//    }

    static class PairWithUrn<T> {
        public final GeniUrn urn;
        public final T object;

        public PairWithUrn(GeniUrn urn, T object) {
            this.urn = urn;
            this.object = object;
        }

        public PairWithUrn(String urnString, T object) {
            this.urn = GeniUrn.parse(urnString);
            assert this.urn != null; //should already be checked by initCollections
            this.object = object;
        }

        public GeniUrn getUrn() {
            return urn;
        }
    }

    /**
     * sort by distance from number of subauths of provided urn. The first has the closest possible subauth to the provided urn.
     */
    static <T> List<PairWithUrn<T>> sortBySubAuthDifference(List<PairWithUrn<T>> l, final GeniUrn urn) {
        l.sort(Comparator.comparing(pair -> getDistance(urn, pair)));
        return l;
    }

    private static <T> int getDistance(GeniUrn urn, PairWithUrn<T> pair) {
        return Math.abs(pair.getUrn().getSubAuthCount() - urn.getSubAuthCount());
    }

    /**
     * Sort by number of sub auths. With smallest number of sub auths first.
     */
    static <T> List<PairWithUrn<T>> sortBySubAuthNumber(List<PairWithUrn<T>> l) {
        l.sort(Comparator.comparing(pair -> pair.getUrn().getSubAuthCount()));
        return l;
    }
}
