package be.iminds.ilabt.jfed.highlevel.model;

import be.iminds.ilabt.jfed.log.ApiCallDetails;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api.AggregateManager3;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.ErrorDetails;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AMv3Wrapper;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.rspec.model.ManifestSliverFinder;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.rspec_source.ManifestRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import javafx.application.Platform;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.text.ParseException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * SfaModelSliceAuthorityListener: this watches for SliceAuthority calls and fills in SfaModel using the info in them
 */
public class SfaModelAggregateManager3Listener extends SfaModelAbstractListener {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(SfaModelAggregateManager3Listener.class);

    private final TestbedInfoSource testbedInfoSource;

    public SfaModelAggregateManager3Listener(SfaModel sfaModel, TestbedInfoSource testbedInfoSource) {
        super(sfaModel);
        this.testbedInfoSource = testbedInfoSource;
    }

    private List<String> getSliceUrns(List<String> urns) {
        List<String> res = new ArrayList<>();
        for (String u : urns) {
            GeniUrn geniUrn = GeniUrn.parse(u);
            if (geniUrn != null && Objects.equals(geniUrn.getEncodedResourceType(), "slice"))
                res.add(u);
        }
        return res;
    }

    private List<String> getSliverUrns(List<String> urn) {
        List<String> res = new ArrayList<>();
        for (String u : urn) {
            GeniUrn geniUrn = GeniUrn.parse(u);
            if (geniUrn != null && Objects.equals(geniUrn.getEncodedResourceType(), "sliver"))
                res.add(u);
        }
        return res;
    }

    private List<String> getSliverUrnsFromSliverInfo(List<AggregateManager3.SliverInfo> sliverinfos) {
        List<String> res = new ArrayList<>();
        for (AggregateManager3.SliverInfo si : sliverinfos)
            res.add(si.getSliverUrn());
        return res;
    }


    private void onGetVersionResult(ApiCallDetails result) {
        if (result.getReply().getGeniResponseCode().isSuccess()) {
            //TODO
        }
    }

    public void onStatusResult(Server auth, AggregateManager3.StatusInfo statusInfo) {
        try {
            getModel().logExistSlice(statusInfo.getSliceUrn());
        } catch (GeniUrn.GeniUrnParseException e) {
            LOG.error("Could not register slice with urn {} from StatusResult", statusInfo.getSliceUrn(), e);
        }

        onListOfSliverInfoResult(auth, statusInfo.getSliceUrn(), statusInfo.getSliverInfo(), false);
    }

    /**
     * @param sliceUrn              urn of the slice the sliver belongs to, may be null if unknown
     * @param ignoreOperationStatus
     */
    public void onListOfSliverInfoResult(Server auth, String sliceUrn, List<AggregateManager3.SliverInfo> sliverinfos, boolean ignoreOperationStatus) {
        assert sliceUrn != null;
        if (sliverinfos == null) return;

        StatusDetails.SliverStatus aggregatedStatusInCall = StatusDetails.SliverStatus.READY;

        Set<GeniUrn> mentionedSliverUrns = new HashSet<>();
        for (AggregateManager3.SliverInfo si : sliverinfos) {
            LOG.debug("");
            if (Objects.equals(si.getAllocationStatus(), "geni_unallocated")) {
                //sliver is deleted
                try {
                    getModel().logNotExistSliver(sliceUrn, si.getSliverUrn(), Sliver.Source.AGGREGATE_MANAGER_STATUS);
                } catch (GeniUrn.GeniUrnParseException e) {
                    LOG.error("Could not register sliver non-existence because of invalid urn {}", si.getSliverUrn(), e);
                }
            } else {
                try {
                    mentionedSliverUrns.add(GeniUrn.parse(si.getSliverUrn()));
                    Sliver sliver = getModel().logExistSliver(sliceUrn, si.getSliverUrn(), Sliver.Source.AGGREGATE_MANAGER_STATUS);

                    StatusDetails statusDetails = sliverInfoToSliverStatus(si);

                    aggregatedStatusInCall =
                            StatusDetails.SliverStatus.merge(aggregatedStatusInCall, statusDetails.getGlobalStatus());

                    if (!ignoreOperationStatus)
                        sliver.setAllocationAndOperationalStatus(
                                si.getAllocationStatus(),
                                si.getOperationalStatus(),
                                sliverInfoToSliverStatus(si));

                    sliver.setExpirationDate(si.getExpiresDate().toInstant());
                } catch (GeniUrn.GeniUrnParseException e) {
                    LOG.error("Could not register sliver existence because of invalid urn {}", si.getSliverUrn(), e);
                } catch (ParseException e) {
                    LOG.warn("in SfaModelAggregateManager3Listener in onListOfSliverInfoResult(): Invalid RFC3339 date in sliver expires: " + si.getExpires(), e);
                }


            }
        }


        //remove slivers that were not mentioned, and are only known via the slice authority
        try {
            Slice slice = getModel().logExistSlice(sliceUrn);

            //force ip-pools to aggregated status in call, and prevent them from being unregistered
            for (Sliver sliver : slice.getSlivers()) {
                if (sliver.getUrn().getEncodedResourceName().startsWith(ManifestSliverFinder.IP_POOL_PREFIX)) {

                    LOG.debug("Explicitly setting status of fake ip-pool sliver {} to {}", sliver.getUrn(), aggregatedStatusInCall);

                    sliver.setStatus(new StatusDetails(aggregatedStatusInCall));
                    mentionedSliverUrns.add(sliver.getUrn());
                }
            }

            List<Sliver> notMentionedSlivers = slice.findSlivers(auth).stream()
                    .filter(sliver -> !mentionedSliverUrns.contains(sliver.getUrn()))
                    .filter(sliver -> sliver.getSource() == Sliver.Source.SLICE_AUTHORITY)
                    .collect(Collectors.toList());

            if (!notMentionedSlivers.isEmpty()) {
                LOG.trace("The following slivers were not mentioned in the SliverInfo of {}: {}",
                        auth.getName(),
                        notMentionedSlivers.stream().map(Sliver::getUrnString).collect(Collectors.joining(", ")));
            } else {
                LOG.trace("No unmentioned slivers in SliverInfo for authority {}", auth);
            }

            for (Sliver notMentionedSliver : notMentionedSlivers) {
                getModel().logNotExistSliver(sliceUrn, notMentionedSliver.getUrnString(), Sliver.Source.AGGREGATE_MANAGER_STATUS);
            }
        } catch (GeniUrn.GeniUrnParseException ex) {
            LOG.error("Failed to remove unnecessary slivers from model", ex);
        }
    }

    public static StatusDetails sliverInfoToSliverStatus(AggregateManager3.SliverInfo si) {
        StatusDetails.SliverStatus s = AMv3Wrapper.getGlobalSliverStatus(si);
        StatusDetails statusDetails = new StatusDetails(s);
        statusDetails.addSliverStatus(si.getSliverUrn(), s);
        if (si.getError() != null && !si.getError().isEmpty())
            statusDetails.addError(new ErrorDetails(si.getError(), si.getSliverUrn()));
        if (si.getExpires() != null)
            try {
                statusDetails.addKnownExpires(si.getExpiresDate());
            } catch (ParseException e) {
                //ignore date parse exception
                LOG.debug("Failed to parse sliver info expires date: " + si.getExpires());
            }
        return statusDetails;
    }

    public void onAllocateAndProvisionInfoResult(Server auth, String sliceUrn, AggregateManager3.AllocateAndProvisionInfo info, boolean ignoreOperationalStatus) {
        onListOfSliverInfoResult(auth, sliceUrn, info.getSliverInfo(), ignoreOperationalStatus);
    }

    public void onManifestInfo(Server auth, AggregateManager3.ManifestInfo manifestInfo) {
        assert manifestInfo != null;
        assert manifestInfo.getSliceUrn() != null;
        assert manifestInfo.getSliverInfos() != null;
        assert manifestInfo.getManifestRspec() != null;
        if(manifestInfo.getSliverInfos().isEmpty())
            LOG.warn("Got a ManifestInfo with empty SliverInfos!");

        List<String> sliverUrns = getSliverUrnsFromSliverInfo(manifestInfo.getSliverInfos());
        seeManifestRspec(auth, manifestInfo.getSliceUrn(), sliverUrns, manifestInfo.getManifestRspec());

        onListOfSliverInfoResult(auth, manifestInfo.getSliceUrn(), manifestInfo.getSliverInfos(), false);
    }

    public void seeAdvertisementRspec(Server server, boolean available, String rspec) {
        getModel().setAdvertisementRspec(server, available, rspec);
    }

//    public void seeRequestRspec(Server auth, String sliceUrn, List<String> sliverUrns, String rspec) {
//        assert sliverUrns != null;
//        assert getModel().getRSpecList() != null;
//        getModel().getRSpecList().seeRequestRspec(auth, sliceUrn, sliverUrns, rspec);
//    }
//
//    public void seeEchoRequestRspec(Server auth, String sliceUrn, List<String> sliverUrns, String rspec) {
//        assert sliverUrns != null;
//        assert getModel().getRSpecList() != null;
//        getModel().getRSpecList().seeEchoRequestRspec(auth, sliceUrn, sliverUrns, rspec);
//    }

    public void seeManifestRspec(@Nonnull Server auth, @Nonnull String sliceUrnStr,
                                 @Nonnull List<String> sliverUrns, @Nonnull String manifestRspecString) {
        //when empty rspec, there will be no slivers.
        if (sliverUrns.isEmpty()) {
            getModel().logNotExistSliverInSliceOnAuth(sliceUrnStr, auth);
            return;
        }

        Slice slice = null;
        try {
            slice = getModel().getSlice(sliceUrnStr);
        } catch (GeniUrn.GeniUrnParseException e) {
            LOG.error("Could not get slice because of an invalid urn {}", sliceUrnStr);
        }

        if (slice == null) {
            LOG.warn("Could not find slice with URN {}, while seeing a Manifest from authority {}", sliceUrnStr,
                    auth.getDefaultComponentManagerUrn());
            return;
        }

        GeniUrn sliceUrn = GeniUrn.parse(sliceUrnStr);
        assert sliceUrn != null;

        //This will find the same list, if sliver_id is set in manifest.
        //This list is not used, but it is still done, here, for the side effect: it maps componet_id and client_id to sliver_id
        findSliversInManifest(auth, sliceUrn, new ManifestRspecSource(manifestRspecString, ModelRspecType.FX));

        List<Sliver> sliverList = new ArrayList<>();
        for (String sliverUrn : sliverUrns) {
            try {
                Sliver sliver =
                        getModel().logExistSliver(sliceUrnStr, sliverUrn, auth, Sliver.Source.AGGREGATE_MANAGER_STATUS); //since success is returned, the sliver(s) must exist
                sliverList.add(sliver);
            } catch (GeniUrn.GeniUrnParseException e) {
                LOG.error("Could not process the invalid sliver-urn {}", sliverUrn, e);
            }
        }

        //////// HACK for PLE ////////
        //hack for ple: handle the fact that ple requires a component_manager_id of
        //   urn:publicid:IDN+ple:ibbtple+authority+cm in the request RSpec but it returns
        //   urn:publicid:IDN+ple+authority+cm
        if (manifestRspecString.contains("component_manager_id=\"urn:publicid:IDN+ple+authority+cm\"")) {
            manifestRspecString = manifestRspecString.replaceAll(
                    Pattern.quote("urn:publicid:IDN+ple+authority+cm"),
                    Matcher.quoteReplacement("urn:publicid:IDN+ple:ibbtple+authority+cm"));
            LOG.warn("Using PLE hack in SfaModelAggregateManager2Listener#seeManifestRspec. succesfully applied: " +
                    (!manifestRspecString.contains("component_manager_id=\"urn:publicid:IDN+ple+authority+cm\"")));
            assert !manifestRspecString.contains("component_manager_id=\"urn:publicid:IDN+ple+authority+cm\"") :
                    "HACK applied unsuccessfully to:\n" + manifestRspecString;
        }
        //////// HACK for PLE ////////

        ManifestRspecSource manifestRspec = new ManifestRspecSource(manifestRspecString, ModelRspecType.FX);
        for (Sliver sliver : sliverList) {
            LOG.debug("SfaModelAggregateManager2Listener seeManifestRspec is setting manifest for " + slice + " -> " + sliceUrnStr + "  " + sliver + " -> " + sliver.getUrn());
            sliver.setManifestRspec(manifestRspec);
        }

//        //sliver exists on all cm's mentioned in manifest. (even if unallocated on others)
//        try {
//            for (String cmUrn : rSpecInfo.getRSpec().getAllComponentManagerUrns()) {
//                Server rspecAuth = getModel().getAuthorityList().getAuthorityListModel().getByUrn(cmUrn);
//                if (rspecAuth == null) {
//                    LOG.error("Error in manifest: contained an unknown authority urn: \"" + cmUrn + "\"");
//                    continue;
//                }
//                getModel().logExistSliverGeniSingle(sliceUrn, rspecAuth); //it is in manifest, so the sliver must exist (even if it is unallocated)
//            }
//        } catch (RspecParseException ex) {
//            LOG.error("Error while processing Rspec XML", ex);
//        }

    }

    @Override
    public void onResult(final ApiCallDetails details) {
//        //Since this triggers updates to the GUI (because it changes Properties) this has to be executed on the JavaFX thread.
//
//        final Lock lock = new ReentrantLock();
//        final Condition waiter = lock.newCondition();
//
//        Platform.runLater(new Runnable() { @Override public void run() {
//            onResultInJavaFXThread(details);
//            lock.lock();
//            waiter.signalAll();
//            lock.unlock();
//        } });
//
//        lock.lock();
//        try {
//            waiter.await();
//        } catch (InterruptedException e) {
//            //TODO: handle?
//            e.printStackTrace();
//        }
//        lock.unlock();
        assert Platform.isFxApplicationThread();
        onResultInJavaFXThread(details);
    }

    public void onResultInJavaFXThread(ApiCallDetails details) {
        //ignore errors here
        if (details.getReply() == null || details.getJavaMethodName() == null)
            return;

        if (!Objects.equals(details.getApiName(), AggregateManager3.getApiName()))
            return;

        LOG.debug("SfaModelAggregateManager3Listener onResultInJavaFXThread API=" + details.getApiName() + " SfaCommand=\"" + details.getGeniMethodName() + "\" javaCommand=\"" + details.getJavaMethodName() + "\"");

        try {
            if (Objects.equals(details.getJavaMethodName(), "getVersion")) //returns VersionInfo
                onGetVersionResult(details);

            if (Objects.equals(details.getJavaMethodName(), "listResources")) { //returns String (rspec)
                if (details.getReply().getGeniResponseCode().isSuccess()) {
                    String rspec = (String) details.getReply().getValue();
                    boolean available = false;
                    if (details.getMethodParameters().get("available") != null)
                        available = (Boolean) details.getMethodParameters().get("available");
                    seeAdvertisementRspec(details.getServer(), available, rspec);
                }
            }

            if (Objects.equals(details.getJavaMethodName(), "describe")) { //returns ManifestInfo
                if (details.getReply().getGeniResponseCode().isSuccess()) {
                    onManifestInfo(details.getServer(), (AggregateManager3.ManifestInfo) details.getReply().getValue());
                }
            }

            if (Objects.equals(details.getJavaMethodName(), "allocate")) { //returns AllocateAndProvisionInfo
                if (details.getReply().getGeniResponseCode().isSuccess()) {
                    AggregateManager3.AllocateAndProvisionInfo info =
                            (AggregateManager3.AllocateAndProvisionInfo) details.getReply().getValue();
                    String sliceUrn = (String) details.getMethodParameters().get("sliceUrn");
                    getModel().logExistSlice(sliceUrn);
                    List<String> sliverUrns = getSliverUrnsFromSliverInfo(info.getSliverInfo());
//                    seeRequestRspec(details.getServer(), sliceUrn, sliverUrns, (String) details.getMethodParameters().get("rspec"));
//                    seeEchoRequestRspec(details.getServer(), sliceUrn, sliverUrns, info.getRspec());
                    onAllocateAndProvisionInfoResult(details.getServer(), sliceUrn, info, false);
                }
            }

            if (Objects.equals(details.getJavaMethodName(), "provision")) { //returns AllocateAndProvisionInfo
                if (details.getReply().getGeniResponseCode().isSuccess()) {
                    AggregateManager3.AllocateAndProvisionInfo info =
                            (AggregateManager3.AllocateAndProvisionInfo) details.getReply().getValue();
                    List<String> urns = (List<String>) details.getMethodParameters().get("urns");
                    List<String> sliceUrns = getSliceUrns(urns);
                    String sliceUrn = sliceUrns.size() != 1 ? null : sliceUrns.get(0);
                    if (sliceUrn != null)
                        getModel().logExistSlice(sliceUrn);

                    //  alternative, get from result instead of arguments:
                    List<String> sliverUrns = getSliverUrnsFromSliverInfo(info.getSliverInfo());
                    //  alternative: get from arguments instead of result:
                    //List<String> sliverUrns = getSliverUrns(urns);

                    seeManifestRspec(details.getServer(), sliceUrn, sliverUrns, info.getRspec());
                    boolean ignoreOperationalStatus =
                            details.getServer().hasFlag(Server.Flag.workaroundIgnoreProvisionOperationalStatus);
                    onAllocateAndProvisionInfoResult(details.getServer(), sliceUrn, info, ignoreOperationalStatus);
                }
            }

            if (Objects.equals(details.getJavaMethodName(), "status")) // returns StatusInfo
            {
                if (details.getReply().getGeniResponseCode().isSuccess()) {
                    //not much to know from urn argument?   List urns = (List) details.getMethodParameters().get("urns");
                    //TODO if single slice URN, and success and at least one sliverinfo, that slice exists...
                    onStatusResult(details.getServer(), (AggregateManager3.StatusInfo) details.getReply().getValue());
                }
            }

            if (Objects.equals(details.getJavaMethodName(), "performOperationalAction")
                    || Objects.equals(details.getJavaMethodName(), "delete")
                    || Objects.equals(details.getJavaMethodName(), "renew")) {
                if (details.getReply().getGeniResponseCode().isSuccess()) {
                    @SuppressWarnings("unchecked")
                    List<AggregateManager3.SliverInfo> si =
                            (List<AggregateManager3.SliverInfo>) details.getReply().getValue();
                    List<String> urns = (List<String>) details.getMethodParameters().get("urns");
                    List<String> sliceUrns = getSliceUrns(urns);
                    String sliceUrn = sliceUrns.size() != 1 ? null : sliceUrns.get(0);
                    onListOfSliverInfoResult(details.getServer(), sliceUrn, si, false);
                }
            }

            if (Objects.equals(details.getJavaMethodName(), "shutdown")) { //returns Boolean
                //ignored
            }
        } catch (Exception e) {
            LOG.warn("WARNING: Exception when processing AggregateManager3 reply for SfaModel. This will be ignored, but it is most likely a bug. " + e.getMessage(), e);
        }
    }
}
