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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.controller.JFedHighLevelException;
import be.iminds.ilabt.jfed.log.ApiCallDetails;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.*;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceId;
import be.iminds.ilabt.jfed.util.GeniUrn;
import javafx.application.Platform;
import org.slf4j.LoggerFactory;

import java.time.Clock;
import java.time.Instant;
import java.util.*;

/**
 * SfaModelSliceAuthorityListener: this watches for SliceAuthority calls and fills in SfaModel using the info in them
 * <p/>
 * listens to both federation API 1 and 2!
 */
public class SfaUniformFederationSliceAuthorityApiListener extends SfaModelAbstractListener {
    private static final org.slf4j.Logger LOG =
            LoggerFactory.getLogger(SfaUniformFederationSliceAuthorityApiListener.class);


    private final TestbedInfoSource testbedInfoSource;

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

    private void onGetSliceCredentials(ApiCallDetails result) {
        assert result.getReply() != null;

        ResourceId sliceId = noteSliceUrnInParameters(result);
        assert sliceId != null;
        noteUserCredentialInParameters(result);

        if (result.getReply().getGeniResponseCode().isSuccess()) {
            String sliceUrn = sliceId.getValue();
            try {
                Slice slice = getModel().getSlice(sliceUrn);
                @SuppressWarnings("unchecked")
                List<AnyCredential> credentialList = (List<AnyCredential>) result.getReply().getValue();

                if (credentialList != null) {
                    if (credentialList.size() > 0)
                        try {
                            slice.setCredentials(credentialList);
                        } catch (JFedHighLevelException e) {
                            LOG.error("Error while registering slice credentials {}", credentialList, e);
                        }

                    credentialList.stream()
                            .filter(credential -> credential instanceof SfaCredential)
                            .filter(credential -> slice.getExpirationDate() == null
                                    || credential.getExpiresDate().toInstant().isAfter(slice.getExpirationDate()))
                            .forEach(credential -> slice.setExpirationDate(credential.getExpiresDate().toInstant()));
                } else
                    LOG.warn("Received an empty credential list for {}", sliceUrn);
            } catch (GeniUrn.GeniUrnParseException e) {
                LOG.error("Cannot process GetSliceCredential because of an invalid sliceUrn {}", sliceUrn);
            }

        }
    }

    private void onLookupSliceInfoResult(ApiCallDetails result) {
        assert result.getReply() != null;

        if (result.getReply().getGeniResponseCode().isSuccess()) {
            assert result.getReply().getValue() instanceof AbstractFederationApi2.SliceInfoList;
            AbstractFederationApi2.SliceInfoList lookupRes =
                    (AbstractFederationApi2.SliceInfoList) result.getReply().getValue();

            for (Map.Entry<GeniUrn, AbstractFederationApi.SliceInfo> entry : lookupRes.entrySet()) {
                AbstractFederationApi2.SliceInfo sliceInfo = entry.getValue();

                Slice slice = getModel().logExistSlice(sliceInfo.getSliceUrn());
                slice.setExpirationDate(sliceInfo.getExpirationDate().toInstant());

                if (sliceInfo.getEmulabHasSlivers() != null) {
                    slice.setHasActiveSlivers(sliceInfo.getEmulabHasSlivers());
                }
                slice.setUuid(sliceInfo.getUuid());

                if (sliceInfo.getCreationDate() != null)
                    slice.setCreationDate(sliceInfo.getCreationDate().toInstant());

            }
        }
    }


    private List<String> getRequestedSlices(ApiCallDetails result) {
        List<String> sliceUrns = new ArrayList<>();

        Object sliceUrnObj = result.getMethodParameters().get("sliceUrn");
        if (sliceUrnObj != null) {
            sliceUrns.add(sliceUrnObj.toString());
        }

        Object matchObj = result.getMethodParameters().get("match");
        if (matchObj instanceof Map) {
            Map<String, ?> match = (Map<String, ?>) matchObj;
            Object sliverInfoSliceUrn = match.get(AbstractFederationApi.SliverInfo.SLIVER_INFO_SLICE_URN);

            if (sliverInfoSliceUrn instanceof String)
                sliceUrns.add((String) sliverInfoSliceUrn);
            else if (sliverInfoSliceUrn instanceof List) {
                List<String> urns = (List<String>) sliverInfoSliceUrn;
                sliceUrns.addAll(urns);
            } else {
                LOG.error("Unexpected value type: {}", sliverInfoSliceUrn.getClass().getName());
            }

        }

        if (sliceUrns.isEmpty())
            LOG.warn("Could not derive sliceUrns from request parameters");

        return sliceUrns;
    }

    private void onLookupSliverInfoResult(ApiCallDetails result) throws JFedException {
        //"match" argument contains info on (possibly multiple) slices

        noteUserCredentialInParameters(result);
                   /*


        if (sliceUrns.isEmpty()) {
            LOG.warn("lookup SLIVER has no match on any slice: deriving info is not yet implemented in this case. " + debug);
            return;
        }
                                         */
        assert result.getReply() != null;

        if (result.getReply().getGeniResponseCode().isSuccess()) {

            AbstractFederationApi.SliverInfoList lookupRes =
                    (AbstractFederationApi.SliverInfoList) result.getReply().getValue();

            lookupRes.values().stream().map(AbstractFederationApi.SliverInfo::getSliceUrn)
                    .forEach(sliceUrn -> {
                        Slice slice = getModel().getSlice(sliceUrn);
                        slice.setHasActiveSlivers(true);
                    });


//            for (String sliceUrnStr : sliceUrns) {
//                GeniUrn sliceUrn = GeniUrn.parse(sliceUrnStr);
//                if (sliceUrn == null) {
//                    LOG.warn("lookup SLIVER will ignore match on invalid slice: urn=\"" + sliceUrnStr + "\" " + debug);
//                    continue;
//                }
//                assert result.getReply() != null;
//                deriveSliceExistence(sliceUrn, result.getReply().getGeniResponseCode(), result.getServer());
//
//
//                //SLIVER_INFO_SLICE_URN 	URN 	URN of slice for registered sliver 	Yes 	Required 	No
//                //SLIVER_INFO_URN 	URN 	URN of registered sliver 	Yes 	Required 	No
//                //SLIVER_INFO_AGGREGATE_URN
//
//                Slice slice;
//                try {
//                    slice = getModel().getSlice(sliceUrnStr);
//                } catch (GeniUrn.GeniUrnParseException e) {
//                    LOG.error("Could not process LookupSliverInfo because of an invalid slice URN {}", sliceUrn);
//                    continue;
//                }
//
//                boolean sawSlice = false;
//                //log existence and non existence of slivers

            for (AbstractFederationApi.SliverInfo sliverInfo : lookupRes.values()) {

                Server auth =
                        testbedInfoSource.getByUrn(sliverInfo.getAggregateUrn(), TestbedInfoSource.SubAuthMatchAllowed.ALLOW_TOPLEVEL, TestbedInfoSource.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY);
                if (auth == null) {
                    LOG.warn("Unknown aggregate returned in SliverInfo: {}", sliverInfo.getAggregateUrn());
                    continue;
                }

                assert Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), sliverInfo.getAggregateUrn());

                if (!Objects.equals(sliverInfo.getSliverUrn().getEncodedTopLevelAuthority_withoutSubAuth(), auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority_withoutSubAuth())) {
                    LOG.warn("Encountered a mismatch between sliver URN and aggregate URN while processing SliverInfo: amUrn={} vs sliverUrn={}", sliverInfo.getAggregateUrn(), sliverInfo.getSliverUrn());
                    continue;
                }

                //log existence of sliver
                Sliver sliver =
                        getModel().logExistSliver(sliverInfo.getSliceUrn(), sliverInfo.getSliverUrn(), auth, Sliver.Source.SLICE_AUTHORITY);

                //if expires unknown, set from this. (thus prefer other sources, and use this as fallback)
                if (sliver != null && sliver.getExpirationDate() == null) {
                    sliver.setExpirationDate(sliverInfo.getExpirationDate().toInstant());
                }
            }
        } else if (result.getReply().getGeniResponseCode() == GeniUniformFederationApiResponseCode.SEARCHFAILED) {
            List<String> requestedSlices = getRequestedSlices(result);
            for (String requestedSliceStr : requestedSlices) {
                try {
                    getModel().logNotExistSliverInSliceOnAuth(new GeniUrn(requestedSliceStr), result.getServer());
                } catch (GeniUrn.GeniUrnParseException e) {
                    LOG.warn("Could not registed slice {} as non-existing on {} because URN could not be parsed", requestedSliceStr, result.getServer().getName());
                }
            }
        }

    }

    private void onGetSlicesForMember1(ApiCallDetails result) throws JFedException {
        noteUserCredentialInParameters(result);

        assert result.getReply() != null;
        if (result.getReply().getGeniResponseCode().isSuccess()) {
            @SuppressWarnings("unchecked")
            AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                    (AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>>) result.getReply();

            if (!reply.getGeniResponseCode().isSuccess() || reply.getValue() == null)
                return;

            List<FederationSliceAuthorityApi1.UrnRoleTuple> urnRoleTuples = reply.getValue();
            for (FederationSliceAuthorityApi1.UrnRoleTuple urnRoleTuple : urnRoleTuples) {
                String sliceUrnString = urnRoleTuple.getUrn().getValue();
                try {
                    getModel().logExistSlice(sliceUrnString);
                } catch (GeniUrn.GeniUrnParseException e) {
                    LOG.error("Could not register slice invalid urn {}", sliceUrnString, e);
                }
            }

            urnRoleTuples.stream()
                    .filter(urnRoleTuple -> !urnRoleTuple.isExpired())
                    .forEach(urnRoleTuple -> {
                        String subAuthName = urnRoleTuple.getUrn().getSubAuthName();
                        if (subAuthName != null)
                            getModel().addAnySubAuthorityName(subAuthName);
                    });
        }
    }

    private void onGetSlicesForMember2(ApiCallDetails result) throws JFedException {
        noteUserCredentialInParameters(result);

        assert result.getReply() != null;
        if (result.getReply().getGeniResponseCode().isSuccess()) {
            @SuppressWarnings("unchecked")
            AbstractFederationApi2.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>> reply =
                    (AbstractFederationApi2.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>>) result.getReply();

            if (!reply.getGeniResponseCode().isSuccess() || reply.getValue() == null)
                return;

            List<FederationSliceAuthorityApi2.UrnRoleTuple> urnRoleTuples = reply.getValue();
            for (FederationSliceAuthorityApi2.UrnRoleTuple urnRoleTuple : urnRoleTuples) {
                String sliceUrnString = urnRoleTuple.getUrn().getValue();
                if (!urnRoleTuple.isExpired()) {
                    try {
                        getModel().logExistSlice(sliceUrnString);
                    } catch (GeniUrn.GeniUrnParseException e) {
                        LOG.error("Could not register slice invalid urn {}", sliceUrnString, e);
                    }
                }
            }

            //also register any subauthorities that we encounter here!
            urnRoleTuples.stream()
                    .filter(urnRoleTuple -> !urnRoleTuple.isExpired())
                    .forEach(urnRoleTuple -> {
                        String subAuthName = urnRoleTuple.getUrn().getSubAuthName();

                        if (subAuthName != null)
                            getModel().addAnySubAuthorityName(subAuthName);
                    });
        }
    }

    private void onGetProjectsForMember2(ApiCallDetails result) throws JFedException {
        noteUserCredentialInParameters(result);

        assert result.getReply() != null;
        if (result.getReply().getGeniResponseCode().isSuccess()) {
            @SuppressWarnings("unchecked")
            AbstractFederationApi2.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>> reply =
                    (AbstractFederationApi2.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>>) result.getReply();

            if (!reply.getGeniResponseCode().isSuccess() || reply.getValue() == null)
                return;

            List<FederationSliceAuthorityApi2.UrnRoleTuple> urnRoleTuples = reply.getValue();
            urnRoleTuples.stream()
                    .filter(urnRoleTuple -> !urnRoleTuple.isExpired())
                    .forEach(urnRoleTuple -> {
                        String subAuthName = urnRoleTuple.getUrn().getEncodedResourceName();

                        if (subAuthName != null)
                            getModel().addLoggedInUserSubAuthorityName(subAuthName);
                    });
        }
    }

    private void onGetProjectsForMember1(ApiCallDetails result) throws JFedException {
        noteUserCredentialInParameters(result);

        assert result.getReply() != null;
        if (result.getReply().getGeniResponseCode().isSuccess()) {
            @SuppressWarnings("unchecked")
            AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                    (AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>>) result.getReply();

            if (!reply.getGeniResponseCode().isSuccess() || reply.getValue() == null)
                return;

            List<FederationSliceAuthorityApi1.UrnRoleTuple> urnRoleTuples = reply.getValue();
            urnRoleTuples.stream()
                    .filter(urnRoleTuple -> !urnRoleTuple.isExpired())
                    .forEach(urnRoleTuple -> {
                        String subAuthName = urnRoleTuple.getUrn().getEncodedResourceName();

                        if (subAuthName != null)
                            getModel().addLoggedInUserSubAuthorityName(subAuthName);
                    });
        }
    }

    /**
     * create_slice probably won't return a credential (not in default return, but could be in extension)! a call to get_credentials is needed
     */
    private void onCreateSlice(ApiCallDetails result) {
        LOG.debug("onCreateSlice -> called");
        noteUserCredentialInParameters(result);
//        ResourceId sliceId = noteSliceUrnInParameters(result); //only on success we can assume slice exists
        //we could also check other failures, for example, when calling register when a slice already exists.
        //but it's not really needed

        assert result.getReply() != null;
        if (result.getReply().getGeniResponseCode().isSuccess()) {
            @SuppressWarnings("unchecked")
            AbstractFederationApi.SliceInfo sliceInfo = (AbstractFederationApi.SliceInfo) result.getReply().getValue();
            Slice slice = getModel().logExistSlice(sliceInfo.getSliceUrn());
            slice.setUuid(sliceInfo.getUuid());
            slice.setCreationDate(slice.getCreationDate() != null ? sliceInfo.getCreationDate().toInstant() : Instant.now()); //fall back to NOW as slice creation date
            slice.setExpirationDate(slice.getExpirationDate() != null ? sliceInfo.getExpirationDate().toInstant() : null);
        }
    }

    private void onGetSliceMembers(ApiCallDetails result) {
        GeniUrn sliceUrn = (GeniUrn) result.getMethodParameters().get("urn");
        assert sliceUrn != null;
        assert result.getReply() != null;

        deriveSliceExistence(sliceUrn, result.getReply().getGeniResponseCode(), result.getServer());
        //user or slice credential here? noteUserCredentialInParameters(result);

        if (result.getReply().getGeniResponseCode().isSuccess() &&
                result.getReply().getValue() instanceof List) {
            Slice slice = getModel().getSlice(sliceUrn);
            assert slice != null;

            @SuppressWarnings("unchecked")
            List<FederationSliceAuthorityApi2.UrnRoleTuple> reply =
                    (List<FederationSliceAuthorityApi2.UrnRoleTuple>) result.getReply().getValue();
            List<GeniUrn> usersUrns = new ArrayList<>();
            for (FederationSliceAuthorityApi2.UrnRoleTuple roleTuple : reply) {
                GeniUrn userUrn = roleTuple.getUrn();
                usersUrns.add(userUrn);
            }
            slice.addUsers(usersUrns);
        }
    }

    private void onGetProjectMembers(ApiCallDetails result) {
        GeniUrn projectUrn = (GeniUrn) result.getMethodParameters().get("urn");
        assert projectUrn != null;
        noteUserCredentialInParameters(result);

        assert result.getReply() != null;
        if (result.getReply().getGeniResponseCode().isSuccess() &&
                result.getReply().getValue() instanceof List) {

            @SuppressWarnings("unchecked")
            List<FederationSliceAuthorityApi2.UrnRoleTuple> reply =
                    (List<FederationSliceAuthorityApi2.UrnRoleTuple>) result.getReply().getValue();
            List<GeniUrn> usersUrns = new ArrayList<>();
            for (FederationSliceAuthorityApi2.UrnRoleTuple roleTuple : reply) {
                GeniUrn userUrn = roleTuple.getUrn();
                usersUrns.add(userUrn);
            }
            getModel().setUsersOfSubAuthority(projectUrn.getEncodedResourceName(), usersUrns);
        }
    }

    @Override
    public void onResult(final ApiCallDetails details) {
        assert Platform.isFxApplicationThread();
        onResultInJavaFXThread(details);
    }

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

        if (!Objects.equals(result.getApiName(), FederationSliceAuthorityApi1.getApiName()) &&
                !Objects.equals(result.getApiName(), FederationSliceAuthorityApi2.getApiName()))
            return;

        try {
            LOG.debug("onResult() methodName=" + result.getJavaMethodName() + " parameters.size(): " + result.getMethodParameters().size());

            if (Objects.equals(result.getApiName(), FederationSliceAuthorityApi1.getApiName())) {
                if (Objects.equals(result.getJavaMethodName(), "getSliceCredentials"))
                    onGetSliceCredentials(result);

                if (Objects.equals(result.getJavaMethodName(), "createSlice"))
                    onCreateSlice(result);

                if (Objects.equals(result.getJavaMethodName(), "lookupSlicesForMember"))
                    onGetSlicesForMember1(result);

//                if (result.getGeniMethodName().equals("lookup_projects"))
//                    onGetProjetForMember1(result);

                if (Objects.equals(result.getJavaMethodName(), "lookupProjectsForMember"))
                    onGetProjectsForMember1(result);
            }
            if (Objects.equals(result.getApiName(), FederationSliceAuthorityApi2.getApiName()) &&
                    result.getMethodParameters().containsKey("type")) {
                LOG.debug("onResult() v2 type=" + result.getMethodParameters().get("type") + "  methodName=" + result.getGeniMethodName() + " javaMethodname=" + result.getJavaMethodName());

                if (Objects.equals(result.getGeniMethodName(), "get_credentials") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLICE"))
                    onGetSliceCredentials(result);

                if (Objects.equals(result.getGeniMethodName(), "lookup_members") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLICE"))
                    onGetSliceMembers(result);

                if (Objects.equals(result.getGeniMethodName(), "lookup_members") &&
                        Objects.equals(result.getMethodParameters().get("type"), "PROJECT"))
                    onGetProjectMembers(result);

                if (Objects.equals(result.getJavaMethodName(), "create") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLICE"))
                    onCreateSlice(result);

                if (Objects.equals(result.getJavaMethodName(), "lookup") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLIVER_INFO"))
                    onLookupSliverInfoResult(result);

                if (Objects.equals(result.getJavaMethodName(), "lookup") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLICE"))
                    onLookupSliceInfoResult(result);


                if (Objects.equals(result.getJavaMethodName(), "lookupForMember") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLICE"))
                    onGetSlicesForMember2(result);

                if (Objects.equals(result.getJavaMethodName(), "lookupForMember") &&
                        Objects.equals(result.getMethodParameters().get("type"), "PROJECT"))
                    onGetProjectsForMember2(result);

                if (Objects.equals(result.getJavaMethodName(), "delete") &&
                        Objects.equals(result.getMethodParameters().get("type"), "SLIVER_INFO"))
                    onDeleteSliverInfo(result);

            }

        } catch (Exception e) {
            LOG.warn("Exception when processing SliceAuthority reply for SfaModel. This will be ignored, but it is most likely a bug. ", e);
        }
    }

    private void onDeleteSliverInfo(ApiCallDetails result) {

        if (result == null || result.getReply() == null)
            return;

        if (result.getReply().getGeniResponseCode().isSuccess()) {
            assert (result.getMethodParameters().containsKey("credentialList"));
            @SuppressWarnings("unchecked")
            List<AnyCredential> sliceCredentials =
                    (List<AnyCredential>) result.getMethodParameters().get("credentialList");
            String sliceUrn = null;
            if (sliceCredentials != null && !sliceCredentials.isEmpty() && sliceCredentials.get(0) instanceof SfaCredential) {
                sliceUrn = ((SfaCredential) sliceCredentials.get(0)).getTargetUrn();
            }

            if (sliceUrn == null) {
                LOG.error("Could not derive sliceUrn from credential");
                return;
            }

            String sliverUrn = (String) result.getMethodParameters().get("urnToDelete");
            assert sliverUrn != null;

            try {
                getModel().logNotExistSliver(sliceUrn, sliverUrn, Sliver.Source.SLICE_AUTHORITY);
            } catch (GeniUrn.GeniUrnParseException ex) {
                LOG.error("Could not unregister sliver with invalid sliver-urn {}", sliverUrn, ex);

            }
        }
    }
}
