package be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.AnyCredential;
import be.iminds.ilabt.jfed.lowlevel.ApiInfo;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractFederationApi;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractFederationApi1;
import be.iminds.ilabt.jfed.lowlevel.api.FederationMemberAuthorityApi1;
import be.iminds.ilabt.jfed.lowlevel.api.FederationSliceAuthorityApi1;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;

/**
 * ProtoGeniSAUserAndSliceApiWrapper
 */
public class UniformFederationApi1UserAndSliceApiWrapper extends UserAndSliceApiWrapper {
    private static final org.slf4j.Logger LOG =
            LoggerFactory.getLogger(UniformFederationApi1UserAndSliceApiWrapper.class);
    @Nullable
    List<AnyCredential> userCredentials;

    public UniformFederationApi1UserAndSliceApiWrapper(@Nonnull Logger logger, @Nonnull GeniUserProvider geniUserProvider, @Nonnull JFedConnectionProvider connectionProvider, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, geniUserProvider, connectionProvider, jFedPreferences);
    }

    @Nonnull
    private FederationMemberAuthorityApi1 ma(@Nonnull Logger logger) {
        return new FederationMemberAuthorityApi1(logger, jFedPreferences);
    }

    @Nonnull
    private FederationSliceAuthorityApi1 sa(@Nonnull Logger logger) {
        return new FederationSliceAuthorityApi1(logger, jFedPreferences);
    }

    private SfaConnection getSaConnection() throws JFedException {
        return getConnection(new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_SA, 1));
    }

    private SfaConnection getMaConnection() throws JFedException {
        return getConnection(new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_MA, 1));
    }

    public FederationSliceAuthorityApi1.GetVersionSAResult getGetVersionSAResult(@Nonnull Logger logger) throws JFedException {
        //TODO: buffer

        AbstractFederationApi1.FederationApiReply<FederationSliceAuthorityApi1.GetVersionSAResult> reply =
                sa(logger).getVersion(getSaConnection());

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("GetVersion call to SA not successful", reply);

        return reply.getValue();
    }

    @Nullable
    @Override
    public List<AnyCredential> getUserCredentials(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        userCredentials = null;

        List<AnyCredential> reqCredCreds = new ArrayList<>();

        if (speaksForCredentials != null && !speaksForCredentials.isEmpty()) {
            reqCredCreds.addAll(speaksForCredentials);
            assert speaksForUserUrn != null;
            user = speaksForUserUrn;
        }

        AbstractFederationApi1.FederationApiReply<List<AnyCredential>> reply =
                ma(logger).getCredentials(getMaConnection(), reqCredCreds, user, null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve user credential from server", reply);

        assert reply.getValue() != null;
        assert !reply.getValue().isEmpty();

        userCredentials = new ArrayList<>();

        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            userCredentials.addAll(speaksForCredentials);

        userCredentials.addAll(reply.getValue());

        return userCredentials;
    }

    @Override
    public boolean hasUserCredentials() {
        return hasUserCredentials(userCredentials);
    }

    @Nullable
    public List<AnyCredential> getCachedUserCredentialsForAM() {
        return userCredentials;
    }

    @Nonnull
    @Override
    public List<AnyCredential> getSliceCredentials(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot fetch SliceCredentials when userCredentials are unknown");

        AbstractFederationApi1.FederationApiReply<List<AnyCredential>> reply =
                sa(logger).getSliceCredentials(getSaConnection(), addSpeaksForCredentials(userCredentials), sliceUrn, null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve slice credential from server", reply);

        List<AnyCredential> sliceCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            sliceCredentials.addAll(speaksForCredentials);
        sliceCredentials.addAll(reply.getValue());
        return sliceCredentials;
    }

    @Nonnull
    @Override
    public List<GeniUrn> getSlicesForUser(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot fetch SlicesForUser when userCredentials are unknown");

        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa(logger).lookupSlicesForMember(getSaConnection(), user, addSpeaksForCredentials(userCredentials), null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve slices for user from server", reply);

        List<GeniUrn> res = new ArrayList<>();
        List<String> resStrings = new ArrayList<>();

        List<FederationSliceAuthorityApi1.UrnRoleTuple> urnRoleTuples = reply.getValue();
        for (FederationSliceAuthorityApi1.UrnRoleTuple urnRoleTuple : urnRoleTuples) {
            res.add(urnRoleTuple.getUrn());
            resStrings.add(urnRoleTuple.getUrn().toString());
        }


        //since it takes only 1 extra call, we'll also get all other info about the slices (such as expire date)
        Map<String, Object> sliceMatch = Collections.singletonMap("SLICE_URN", resStrings);
        sa(logger).lookupSlices(getSaConnection(), addSpeaksForCredentials(userCredentials), sliceMatch, null, null);

        return res;
    }

    @Nonnull
    public List<GeniUrn> getUserProjects(@Nonnull Logger logger, GeniUrn user) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot fetch UserProjects when userCredentials are unknown");


        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa(logger).lookupProjectsForMember(getSaConnection(), user,
                        addSpeaksForCredentials(userCredentials), null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve slices for user from server", reply);

        List<GeniUrn> res = new ArrayList<>();

        List<FederationSliceAuthorityApi1.UrnRoleTuple> urnRoleTuples = reply.getValue();
        boolean hasExpired = false;
        for (FederationSliceAuthorityApi1.UrnRoleTuple urnRoleTuple : urnRoleTuples) {

            //first a special exception: server might include EXPIRED field in tuple. We can use that info
            if (urnRoleTuple.isExpired()) {
                hasExpired = true;
                LOG.debug("skipping expired project: " + urnRoleTuple.getUrn());
                continue;
            }

            res.add(urnRoleTuple.getUrn());
        }

        if (!hasExpired) {
            LOG.debug("Doing second call to check for expired projects");

            Map<String, Object> match;
            assert getLoggedInUserAuthorityServer() != null;
            if (getLoggedInUserAuthorityServer().hasFlag(Server.Flag.workaroundFedMaMatchProjectExpired))
                match = Collections.singletonMap("EXPIRED", Boolean.FALSE);
            else
                match = Collections.emptyMap();
            List<String> filter = Collections.singletonList("PROJECT_URN");

            try {
                AbstractFederationApi1.FederationApiReply<AbstractFederationApi.ProjectInfoList> nonExpiredProj =
                        sa(logger).lookupProjects(getSaConnection(),
                                addSpeaksForCredentials(userCredentials),
                                match,
                                filter,
                                null);

                if (nonExpiredProj.getGeniResponseCode().isSuccess() && nonExpiredProj.getValue() != null) {
                    LOG.debug("Found " + nonExpiredProj.getValue().size() + " non expired projects on server.");
                    res.removeIf(projUrn -> !nonExpiredProj.getValue().containsKey(projUrn));
                } else {
                    LOG.warn("lookup_projects call failed. Could not check for expired projects. This error will be ignored (possibly leaving expired projects in the list).");
                }

            } catch (AssertionError | Exception e) {
                LOG.warn("lookup_projects call failed. Could not check for expired projects. This error will be ignored (possibly leaving expired projects in the list).", e);
            }

        } else {
            LOG.debug("No second call needed to check for expired projects");
        }

        LOG.debug("UniformFederationApi1UserAndSliceApiWrapper#getUserProjects res.size()={}", res.size());
        return res;
    }

    public AbstractFederationApi.MemberInfoList getUserIdentifyingInfo(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot fetch userIdentifyingInfo when userCredentials are unknown");
        if (getRelevantUserUrnString() == null)
            throw new IllegalStateException("No relevateUserUrnString available to do getUserIdentifiyingInfo call");

        Map<String, String> match = Collections.singletonMap("MEMBER_URN", user.getValue());

        AbstractFederationApi.FederationApiReply<AbstractFederationApi.MemberInfoList> reply =
                ma(logger).lookupIdentifyingMemberInfo(getMaConnection(), addSpeaksForCredentials(userCredentials),
                        match, null, null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Problem looking up user info for " + user, reply);

        return reply.getValue();
    }

    @Nullable
    @Override
    public List<GeniUrn> getAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException {
        AbstractFederationApi.FederationApiReply<AbstractFederationApi.SliverInfoList> reply =
                sa(logger).lookupSliverInfo(getSaConnection(), sliceCredentials, sliceUrn, null, null, null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Problem looking up aggregates for slice '" + sliceUrn + "'", reply);

        return reply.getValue().values().stream()
                .map(AbstractFederationApi.SliverInfo::getAggregateUrn)
                .collect(Collectors.toList());
    }

    @Nullable
    @Override
    public Date getSliceExpiration(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException {
        //not implemented: null will cause fall back to using credential in AutomaticUserAndSliceApiWrapper
        return null;
    }

    @Override
    public void shareSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn userUrn) throws JFedException {
        List<FederationSliceAuthorityApi1.UrnRoleTuple> membersToAdd =
                new ArrayList<>();
        membersToAdd.add(new FederationSliceAuthorityApi1.UrnRoleTuple(userUrn, "MEMBER"));

        AbstractFederationApi.FederationApiReply<String> reply =
                sa(logger).modifySliceMembership(getSaConnection(),
                        sliceUrn,
                        membersToAdd,
                        new ArrayList<>() /*membersToRemove*/,
                        new ArrayList<>() /*membersToChange*/,
                        sliceCredentials,
                        null
                );

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (reply.getOutput() != null && reply.getOutput().contains("DUPLICATE")) {
                LOG.warn("Reply to shared slice was \"" + reply.getOutput() + "\". Assuming this means it is successfully shared already.");
                return;
            }
            throw new JFedException("Sharing slice with " + userUrn + " did not succeed", reply);
        }
    }

    @Override
    public List<GeniUrn> getUsersForSubAuthority(@Nonnull Logger logger, @Nonnull String projectName) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot fetch usersForSubAuthority when userCredentials are unknown");

        assert getLoggedInUserAuthorityServer() != null;
        GeniUrn projectUrn =
                GeniUrn.createGeniUrnFromEncodedParts(getLoggedInUserAuthorityServer().getUrnTld(), "project", projectName);
        AbstractFederationApi.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa(logger).lookupProjectMembers(getSaConnection(), projectUrn, addSpeaksForCredentials(userCredentials), null);

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("Error getting users in project " + projectUrn, reply);
        }

        return reply.getValue().stream()
                .map(FederationSliceAuthorityApi1.UrnRoleTuple::getUrn)
                .collect(Collectors.toList());
    }

    @Override
    public List<GeniUrn> getUsersForSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        AbstractFederationApi.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa(logger).lookupSliceMembers(getSaConnection(), sliceUrn, sliceCredentials, null);

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("Error getting users in " + sliceUrn, reply);
        }

        return reply.getValue().stream().map(FederationSliceAuthorityApi1.UrnRoleTuple::getUrn).collect(Collectors.toList());
    }

    @Override
    public void addUsersToSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull List<GeniUrn> userUrns, @Nonnull String role) throws JFedException {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Nonnull
    @Override
    public SliceInfo createSlice(@Nonnull Logger logger, @Nonnull String sliceName, @Nullable Date expirationDate, @Nullable String subAuthName) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot do createSlice when userCredentials are unknown");

        //2 steps: create slice, and then get its credential(s)

        SfaConnection con = getSaConnection();

        FederationSliceAuthorityApi1.GetVersionSAResult gv = getGetVersionSAResult(logger);
        assert gv != null;

        Map<String, String> fields = new HashMap<>();
        fields.put("SLICE_DESCRIPTION", "jFed Experimenter GUI slice '" + sliceName + "' for user " + getRelevantUserUrnString());

        GeniUrn chosenProject = null;
        boolean projectSupported = true;//gv.getServices().contains("PROJECT");
        if (projectSupported) {
            if (subAuthName == null)
                throw new JFedException("Slice creation requires a project, but user did not specify any project.");

            assert getRelevantUserUrn() != null;
            chosenProject =
                    GeniUrn.createGeniUrnFromEncodedParts(getRelevantUserUrn().getEncodedTopLevelAuthority(), "project", subAuthName);
        }
        if (!projectSupported && subAuthName != null) {
            LOG.warn("The server does not support PROJECTS, but the user specified a project name. Will ignore project");
        }
        if (chosenProject != null)
            fields.put("SLICE_PROJECT_URN", chosenProject.getValue());

        if (gv.getFieldsForObject(FederationSliceAuthorityApi1.TYPE_PROJECT).containsKey("_GENI_SLICE_EMAIL")) {
            AbstractFederationApi.MemberInfoList userIdentifyingInfo =
                    getUserIdentifyingInfo(logger, getRelevantUserUrn());

            if (userIdentifyingInfo != null && ! userIdentifyingInfo.get(getRelevantUserUrn()).getEmail().isEmpty()) {
                String userEmail = userIdentifyingInfo.get(getRelevantUserUrn()).getEmail();
                fields.put("_GENI_SLICE_EMAIL", userEmail);
            } else {
                throw new JFedException("Slice creation requires a user email, but the user email is not known.");
            }
        }

//      TODO  addRequiredSupplementaryFieldsToCreate(fields, "SLICE");

        if (expirationDate != null) {
            //note: SLICE_EXPIRATION is allowed on Creation, so this will always work
            fields.put("SLICE_EXPIRATION", RFC3339Util.dateToRFC3339String(expirationDate, true, true, true));
        }

        AbstractFederationApi.FederationApiReply<FederationSliceAuthorityApi1.SliceInfo> reply =
                sa(logger).createSlice(
                        con,
                        addSpeaksForCredentials(userCredentials),
                        sliceName/* automatically added to "SLICE_NAME" field*/,
                        fields,
                        null);


        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Error creating slice", reply);

        FederationSliceAuthorityApi1.SliceInfo sliceInfo = reply.getValue();

//            if (sliceInfo.containsKey("SLICE_CREDENTIAL"?)) {
        //        //TODO slice credential is normally in returned fields? Otherwise, we can retrieve it with a call.
//            }


        AbstractFederationApi1.FederationApiReply<List<AnyCredential>> credReply =
                sa(logger).getSliceCredentials(con, addSpeaksForCredentials(userCredentials), sliceInfo.getSliceUrn(), null);

        List<AnyCredential> sliceCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            sliceCredentials.addAll(speaksForCredentials);
        sliceCredentials.addAll(credReply.getValue());

        //TODO support multiple credentials
        return new SliceInfo(sliceName, sliceInfo.getSliceUrn(), sliceCredentials);
    }

    @Nonnull
    @Override
    public List<AnyCredential> renewSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Date newExpirationDate) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot do renewSlice when userCredentials are unknown");

        GeniUrn sliceUrn;
        try {
            sliceUrn = findSliceUrn(sliceCredentials);
        } catch (IllegalArgumentException e) {
            throw new JFedException("Could not find slice URN in provided slice credential list.");
        }

        Map<String, String> fields =
                Collections.singletonMap("SLICE_EXPIRATION", RFC3339Util.dateToRFC3339String(newExpirationDate));
        AbstractFederationApi.FederationApiReply<String> updateReply = sa(logger).
                updateSlice(getSaConnection(), sliceCredentials, sliceUrn, fields, null);

        if (!updateReply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to update SLICE_EXPIRATION", updateReply);

        AbstractFederationApi1.FederationApiReply<List<AnyCredential>> credReply =
                sa(logger).getSliceCredentials(getSaConnection(), addSpeaksForCredentials(userCredentials), sliceUrn, null);

        if (!credReply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to get updated slice credentials", credReply);

        List<AnyCredential> newSliceCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            newSliceCredentials.addAll(speaksForCredentials);
        newSliceCredentials.addAll(credReply.getValue());
        return newSliceCredentials;
    }

    @Nonnull
    @Override
    public List<String> getSshKeysForUser(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException {
        if (userCredentials == null)
            throw new IllegalStateException("Cannot fetch sshKeysForUser when userCredentials are unknown");

        Map<String, String> match = Collections.singletonMap("KEY_MEMBER", userUrn.toString());
        AbstractFederationApi.FederationApiReply<Map<GeniUrn, List<AbstractFederationApi.MemberKeyInfo>>>
                reply =
                ma(logger).lookupKeys(getMaConnection(), addSpeaksForCredentials(userCredentials), match, null/*filter*/, null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to get SSH keys", reply);

        return reply.getValue().get(userUrn).stream()
                .map(FederationMemberAuthorityApi1.MemberKeyInfo::getPublicKey)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }


    /* sub authority support */
    @Nonnull
    @Override
    public SubAuthoritySupport getSubAuthoritySupport(@Nonnull Logger logger) throws JFedException {
        FederationSliceAuthorityApi1.GetVersionSAResult getSAVersionRes = getGetVersionSAResult(logger);
        if (!getSAVersionRes.getServices().contains(FederationSliceAuthorityApi1.TYPE_PROJECT))
            return SubAuthoritySupport.SUB_AUTHORITY_FORBIDDEN;
        else
            return SubAuthoritySupport.SUB_AUTHORITY_MANDATORY;
    }

    @Nonnull
    @Override
    public List<String> getSubAuthorityNames(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        List<String> res =
                getUserProjects(logger, user).stream().map(GeniUrn::getEncodedResourceName).collect(Collectors.toList());

        LOG.debug("Returning subauthorities: {}", res);
        return res;
    }

    @Override
    public boolean isRegisterAggregatesForSliceSupported() {
        return false;
    }

    @Override
    public boolean hasSpeaksForSupport() {
        return true;
    }

    @Override
    public void setSpeaksFor(List<AnyCredential> speaksForCredentials, @Nullable GeniUrn speakingForUrn) {
        super.setSpeaksFor(speaksForCredentials, speakingForUrn);
        if (speakingForUrn != null)
            setExtraOptionsForCallsWithCredential(Collections.singletonMap("speaking_for", speakingForUrn.getValue()));
    }
}
