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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Service;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.ProtogeniSliceAuthority;
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.JFedTrustStore;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import org.slf4j.LoggerFactory;

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

/**
 * ProtoGeniSAUserAndSliceApiWrapper
 */
public class ProtoGeniSAUserAndSliceApiWrapper extends UserAndSliceApiWrapper {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(ProtoGeniSAUserAndSliceApiWrapper.class);
    private static final ApiInfo.Api API_INFO = new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_SA, 1);
    private List<AnyCredential> userCredentialsNoSubAuth;
    private final Map<String, List<AnyCredential>> userCredentialsBySubAuth = new HashMap<>();
    @Nullable
    private HashMap<String, String> subAuthNamesToUrl = null;

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

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

    @Nonnull
    protected SfaConnection getConnection(@Nonnull Logger logger, @Nullable String subAuthName) throws JFedException {
        if (subAuthName == null)
            return getConnection(API_INFO);
        else {

            if (subAuthNamesToUrl == null) {
                if (userCredentialsNoSubAuth == null)
                    throw new JFedException("There are no user credentials stored yet, while that is needed to request the names of the sub authorities");

                GeniUrn userUrn;
                if (speaksForUserUrn != null) {
                    userUrn = speaksForUserUrn;
                } else
                    userUrn = geniUserProvider.getLoggedInGeniUser().getUserUrn();

                getSubAuthorityNames(logger, userUrn);
            }

            if (subAuthNamesToUrl == null)
                throw new JFedException("Error in ProtoGeniSAUserAndSliceApiWrapper getSubAuthority: no sub authorities known. subAuthName=" + subAuthName);

            String subAuthUrlStr = subAuthNamesToUrl.get(subAuthName);
            if (subAuthUrlStr == null)
                throw new JFedException("Error in ProtoGeniSAUserAndSliceApiWrapper getSubAuthority: no sub authority for subAuthName=" + subAuthName);

            URL subAuthUrl;
            try {
                subAuthUrl = new URL(subAuthUrlStr);
            } catch (MalformedURLException e) {
                throw new JFedException("Error in ProtoGeniSAUserAndSliceApiWrapper getSubAuthority: no valid URL for subAuthName=" + subAuthName + " subAuthUrlStr=\"" + subAuthUrlStr + "\"", e);
            }


            GeniUser user = geniUserProvider.getLoggedInGeniUser();
            assert user.getUserAuthorityServer() != null;

            JFedTrustStore jFedTrustStore = new JFedTrustStore();
            jFedTrustStore.addTrustedPemCertificateIfNotAdded(user.getUserAuthorityServer().getCertificateChain());

            Service service = ApiInfo.findService(user.getUserAuthorityServer(), ApiInfo.ApiName.PROTOGENI_SA, 1);

            return (SfaConnection) connectionProvider.getConnectionByUrl(jFedTrustStore, user, subAuthUrl,
                    null, API_INFO, service, null);
        }
    }

    @Nonnull
    public List<AnyCredential> getUserCredentials(@Nonnull Logger logger, GeniUrn userUrn, @Nullable String subAuthorityName) throws JFedException {
        if (subAuthorityName != null)
            assert userCredentialsNoSubAuth != null;

        SfaConnection con = getConnection(logger, subAuthorityName);
        assert con.getDebugInfo() != null && con.getDebugInfo().getService() != null;

        ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> reply;
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).getCredential(con);
        } else {
            reply = sa(logger).getAnyCredential(con, speaksForCredentials, null, null);

        }

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

        List<AnyCredential> res = new ArrayList<>();
        res.add(reply.getValue());

        if (speaksForCredentials != null && !speaksForCredentials.isEmpty()) {
            res.addAll(speaksForCredentials);
        }

        if (subAuthorityName == null)
            this.userCredentialsNoSubAuth = res;
        else
            this.userCredentialsBySubAuth.put(subAuthorityName, res);
        return new ArrayList<>(res);
    }

    @Nonnull
    public List<AnyCredential> getCachedUserCredentialsForAM() {
        if (userCredentialsNoSubAuth == null)
            return Collections.emptyList();
        else
            return new ArrayList<>(userCredentialsNoSubAuth);
    }

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

    protected List<AnyCredential> getSubAuthCredentials(@Nonnull Logger logger, String subAuthName) throws JFedException {
        List<AnyCredential> res = userCredentialsBySubAuth.get(subAuthName);
        if (res == null) {
            res = getUserCredentials(logger, geniUserProvider.getLoggedInGeniUser().getUserUrn(), subAuthName);
        }
        if (res.isEmpty() || !hasUserCredentials(res)) {
            res = getUserCredentials(logger, geniUserProvider.getLoggedInGeniUser().getUserUrn(), subAuthName);
        }
        return res;
    }

    @Nonnull
    @Override
    public List<AnyCredential> getUserCredentials(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException {
        return getUserCredentials(logger, userUrn, null/*subAuthorityName*/);
    }

    @Nonnull
    @Override
    public List<AnyCredential> getSliceCredentials(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn) throws JFedException {
        List<AnyCredential> userCredentials_forCall = getCredentialsForCall(logger, sliceUrn);

        ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> reply;
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).getSliceCredential(getConnection(logger, sliceUrn.getEncodedSubAuthName()),
                    AnyCredential.toSingleCredential(userCredentials_forCall), sliceUrn);
        } else {
            reply = sa(logger).getSliceCredential(getConnection(logger, sliceUrn.getEncodedSubAuthName()),
                    userCredentials_forCall, sliceUrn);
        }

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

        if (speaksForCredentials == null || speaksForCredentials.isEmpty())
            return Collections.singletonList(reply.getValue());
        else {
            List<AnyCredential> res = new ArrayList<>();
            res.add(reply.getValue());
            res.addAll(speaksForCredentials);
            return res;
        }
    }

    @Nonnull
    private List<AnyCredential> getCredentialsForCall(@Nonnull Logger logger, @Nonnull GeniUrn urn) throws JFedException {
        return getCredentialsForCall(logger, urn.getEncodedSubAuthName());
    }

    @Nonnull
    private List<AnyCredential> getCredentialsForCall(@Nonnull Logger logger, @Nullable String subAuthName) throws JFedException {
        List<AnyCredential> userCredentials_forCall;
        if (subAuthName == null) {
            userCredentials_forCall = userCredentialsNoSubAuth;
        } else {
            //use subauth credentials for call
            userCredentials_forCall = getSubAuthCredentials(logger, subAuthName);
        }
        if (userCredentials_forCall == null)
            throw new JFedException("Internal jFed error: no correct credential stored. (subAuthName=" + subAuthName + ")");
        return userCredentials_forCall;
    }

    @Nonnull
    @Override
    public List<GeniUrn> getSlicesForUser(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        ProtogeniSliceAuthority.SliceAuthorityReply<ProtogeniSliceAuthority.UserInfo> reply;

        if (!hasUserCredentials()) {
            getUserCredentials(logger, user);
            assert hasUserCredentials();
        }
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).resolveUser(getConnection(logger, null),
                    AnyCredential.toSingleCredential(userCredentialsNoSubAuth), user);
        } else {
            reply = sa(logger).resolveUser(getConnection(logger, null),
                    userCredentialsNoSubAuth, user);
        }

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

        return reply.getValue().getSlices();
    }

    @Nonnull
    @Override
    public List<GeniUrn> getAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException {
        List<AnyCredential> userCredentials_forCall = getCredentialsForCall(logger, sliceUrn);

        ProtogeniSliceAuthority.SliceAuthorityReply<ProtogeniSliceAuthority.SliceInfo> reply;

        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).resolveSlice(getConnection(logger, sliceUrn.getEncodedSubAuthName()),
                    AnyCredential.toSingleCredential(userCredentials_forCall), sliceUrn);
        } else {
            reply = sa(logger).resolveSlice(getConnection(logger, sliceUrn.getEncodedSubAuthName()),
                    userCredentials_forCall, sliceUrn);
        }

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve aggregates for slice", reply);

        return reply.getValue().getComponentManagers();
    }

    @Nullable
    @Override
    public Date getSliceExpiration(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException {
        //Resolve of slice does not return expiration
        return null;
    }

    @Override
    public void shareSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn userUrn) throws JFedException {
        ProtogeniSliceAuthority.SliceAuthorityReply<Boolean> reply;
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).bindToSlice(getConnection(logger, sliceUrn.getEncodedSubAuthName()),
                    AnyCredential.toSingleCredential(sliceCredentials), userUrn);
        } else {
            reply = sa(logger).bindToSlice(getConnection(logger, sliceUrn.getEncodedSubAuthName()),
                    sliceCredentials, userUrn);
        }

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("shareSlice call not succesful", reply);
    }

    @Nonnull
    @Override
    public SliceInfo createSlice(@Nonnull Logger logger, @Nonnull String sliceName, @Nullable Date expirationDate, @Nullable String subAuthName) throws JFedException {
        List<AnyCredential> userCredentials_forCall = getCredentialsForCall(logger, subAuthName);

        if (getLoggedInUserAuthorityServer() == null)
            throw new IllegalStateException("Cannot createSlice when User Authority-server is unknown");
        GeniUrn sliceUrn;
        if (subAuthName != null)
            sliceUrn =
                    GeniUrn.createGeniUrnFromEncodedParts(getLoggedInUserAuthorityServer().getUrnTld() + ":" + subAuthName, "slice", sliceName);
        else
            sliceUrn =
                    GeniUrn.createGeniUrnFromEncodedParts(getLoggedInUserAuthorityServer().getUrnTld(), "slice", sliceName);

        ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> reply;
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).register(getConnection(logger, subAuthName),
                    AnyCredential.toSingleCredential(userCredentials_forCall), sliceUrn);
        } else {
            reply = sa(logger).register(getConnection(logger, subAuthName), userCredentials_forCall, sliceUrn);
        }

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not create slice on server", reply);

        AnyCredential sliceCredential = reply.getValue();
        boolean mustRenew = expirationDate != null;
        if (expirationDate != null && sliceCredential instanceof SfaCredential) {
            SfaCredential sfaCredential = (SfaCredential) sliceCredential;
            mustRenew = sfaCredential.getExpiresDate() != null && sfaCredential.getExpiresDate().before(expirationDate);
        }

        if (mustRenew) {
            try {
                ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> replyRenew =
                        sa(logger).renewSlice(getConnection(logger, subAuthName), sliceCredential,
                                RFC3339Util.dateToRFC3339String(expirationDate, true, true, true));

                if (replyRenew.getGeniResponseCode().isSuccess()) {
                    sliceCredential = replyRenew.getValue(); //credential for new expire time
                } else {
                    LOG.warn("Renew call to set expiration of new Slice date failed. Ignoring. Output: " + replyRenew.getOutput());
                }
            } catch (JFedException e) {
                LOG.error("Ignoring exception when renewing: " + e.getMessage(), e);
            }
        }

        GeniUrn returnedSliceUrn = sliceUrn; //fallback
        if (sliceCredential instanceof SfaCredential) {
            SfaCredential sfaCred = (SfaCredential) sliceCredential;
            try {
                returnedSliceUrn = new GeniUrn(sfaCred.getTargetUrn());
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new JFedException("Could not parse the returned SliceUrn '" + sfaCred.getTargetUrn() + "'", reply);
            }
            if (!Objects.equals(returnedSliceUrn, sliceUrn))
                LOG.warn("URN CHANGED: \"Register\" of slice with urn \"" + sliceUrn + "\" returned credential for URN \"" + returnedSliceUrn + "\"");
        }

        List<AnyCredential> sliceCredentials = new ArrayList<>();
        sliceCredentials.add(sliceCredential);
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            sliceCredentials.addAll(speaksForCredentials);
        return new SliceInfo(sliceName, returnedSliceUrn, sliceCredentials);
    }

    @Nonnull
    @Override
    public List<AnyCredential> renewSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Date newExpirationDate) throws JFedException {
        GeniUrn sliceUrn;
        String subAuthName;
        try {
            sliceUrn = findSliceUrn(sliceCredentials);
            subAuthName = sliceUrn.getEncodedSubAuthName();

        } catch (IllegalArgumentException e) {
            LOG.warn("renewSlice: could not find sliceUrn in credential list. cannot determine if this is slice for subAuthority! Assuming no", e);
            subAuthName = null;
        }

        //assuming that when subauth slice credential is known, URL of subAuth is also known
        if (subAuthName != null)
            assert subAuthNamesToUrl != null;

        ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> reply;
        if (speaksForCredentials == null || speaksForCredentials.isEmpty())
            reply = sa(logger).renewSlice(getConnection(logger, subAuthName),
                    AnyCredential.toSingleCredential(sliceCredentials),
                    RFC3339Util.dateToRFC3339String(newExpirationDate, true, true, true));
        else
            reply = sa(logger).renewSlice(getConnection(logger, subAuthName),
                    sliceCredentials,
                    RFC3339Util.dateToRFC3339String(newExpirationDate, true, true, true));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not renew slice on server: code=" +
                    reply.getGeniResponseCode() + " output=" + reply.getOutput(),
                    reply.getXMLRPCCallDetailsWithCodeValueError(), reply.getGeniResponseCode()
            );

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

    @Override
    public List<String> getSshKeysForUser(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException {
        if (!Objects.equals(geniUserProvider.getLoggedInGeniUser().getUserUrn(), userUrn))
            throw new JFedException("Protogeni SA API can NOT get keys for other user than logged in user.");

        ProtogeniSliceAuthority.SliceAuthorityReply<List<String>> reply =
                sa(logger).getKeys(getConnection(logger, null),
                        AnyCredential.toSingleCredential(userCredentialsNoSubAuth));

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

        return reply.getValue();

    }

    @Nonnull
    @Override
    public SubAuthoritySupport getSubAuthoritySupport(@Nonnull Logger logger) throws JFedException {
        return SubAuthoritySupport.SUB_AUTHORITY_OPTIONAL;
    }

    /**
     * Example in ResolveUser reply:
     * "subauthorities" -> {
     * "urn:publicid:IDN+wall2.ilabt.iminds.be:odspracticum+authority+sa" -> "https://www.wall2.ilabt.iminds.be:12369/protogeni/xmlrpc/project/ODSPracticum/sa",
     * "urn:publicid:IDN+wall2.ilabt.iminds.be:fed4fire+authority+sa" -> "https://www.wall2.ilabt.iminds.be:12369/protogeni/xmlrpc/project/Fed4Fire/sa"
     * },
     */
    @Nonnull
    @Override
    public List<String> getSubAuthorityNames(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        ProtogeniSliceAuthority.SliceAuthorityReply<ProtogeniSliceAuthority.UserInfo> reply;
        if (speaksForCredentials == null || speaksForCredentials.isEmpty()) {
            reply = sa(logger).resolveUser(getConnection(logger, null),
                    AnyCredential.toSingleCredential(userCredentialsNoSubAuth), user);
        } else {
            reply = sa(logger).resolveUser(getConnection(logger, null),
                    userCredentialsNoSubAuth, user);
        }

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("getSubAuthorityNames call not succesful", reply);

        Map<GeniUrn, String> subAuthorities = reply.getValue().getSubAuthorities();

        return reply.getValue().getSubAuthorities().keySet().stream()
                .map(GeniUrn::getSubAuthName)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());

    }

    @Nonnull
    @Override
    public List<GeniUrn> getUsersForSubAuthority(@Nonnull Logger logger, @Nonnull String projectName) throws JFedException {
        throw new UnsupportedOperationException("Not supported by API");
    }

    @Nonnull
    @Override
    public List<GeniUrn> getUsersForSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        throw new UnsupportedOperationException("Not supported by API");
    }

    @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");
    }

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

    @Override
    public boolean hasSpeaksForSupport() {
        //the Protogeni API supports speaksFor
        return true;
    }
}
