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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
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 org.slf4j.LoggerFactory;

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

/**
 * UserAndSliceApiWrapper wraps user and slice CRU calls.
 * <p/>
 * It stores user the credentials needed internally and caches them.
 * The reason for this is that there might be multiple types of credentials, depending on the API.
 * <p/>
 * Slice credentials are not stored. (might change in future version, but no reason to do that yet)
 * <p/>
 * This an currently wrap:
 * - "ProtoGeni Slice Authority API v1"
 * - "Uniform Federation Slice Authority API draft"+"Uniform Federation Member Authority API draft"
 * - Planetlab SfaRegistryInterface
 * <p/>
 * Some calls might result in multiple calls.
 * <p/>
 * Most call returns some "high-level" useful result.
 * But it is also assumes that users of this API process the call results themselves as well if they need more details. (jFed does this in the highlevel-library)
 */
public abstract class UserAndSliceApiWrapper extends ApiWrapper {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(UserAndSliceApiWrapper.class);
    public static final int CREDENTIAL_EXPIRATION_MARGIN_IN_MS = 2 * 60 * 1000;

    @Nullable
    protected Map<String, ?> extraOptionsForCallsWithCredential;

    /**
     * Note: logger is not a parameter with a very good reason!
     * <p/>
     * if it was, it would be a problem to make parallel calls.
     * The reason is that the UserAndSliceApiWrapper stores credential data, so it cannot be recreated each time its used, like the AMWrapper
     * So the logger has to be changed for each call, but that is hard if the logger is stored in the wrapper.
     * Solution: each call needs the specific logger. (this is also the method least likely to cause bugs due to confusion)
     */
    public UserAndSliceApiWrapper(@Nonnull Logger logger, @Nonnull GeniUserProvider geniUserProvider, @Nonnull JFedConnectionProvider connectionProvider, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, geniUserProvider, connectionProvider, jFedPreferences);
    }


    @Nonnull
    protected SfaConnection getConnection(@Nonnull ApiInfo.Api api) throws JFedException {
        GeniUser user = geniUserProvider.getLoggedInGeniUser();
        Server userAuthServer = user.getUserAuthorityServer();
        assert userAuthServer != null;
        return (SfaConnection) connectionProvider.getConnectionByAuthority(user, userAuthServer, api);
    }

    /**
     * You seldomly want the logged in user, you want the spoken for user in speaksfor
     */
    protected GeniUser getLoggedInUser() {
        return geniUserProvider.getLoggedInGeniUser();
    }

    @Nullable
    protected Server getLoggedInUserAuthorityServer() {
        GeniUser user = geniUserProvider.getLoggedInGeniUser();
        return user.getUserAuthorityServer();
    }

    @Nullable
    protected GeniUrn getRelevantUserUrn() {
        if (speaksForUserUrn != null) return speaksForUserUrn;
        return geniUserProvider.getLoggedInGeniUser().getUserUrn();
    }

    @Nullable
    protected String getRelevantUserUrnString() {
        GeniUrn u = getRelevantUserUrn();
        return u == null ? null : u.toString();
    }

    /**
     * Get the credentials of the logged in user, or in the speaksFor case, of the user spoken for
     */
    public List<AnyCredential> getLocalUserCredentials(@Nonnull Logger logger) throws JFedException {
        assert getRelevantUserUrn() != null;
        return getUserCredentials(logger, getRelevantUserUrn());
    }

    public abstract boolean hasUserCredentials();

    public abstract boolean hasSpeaksForSupport();

    @Override
    public void setSpeaksFor(@Nullable List<AnyCredential> speaksForCredentials, @Nullable GeniUrn speakingForUrn) {
        super.setSpeaksFor(speaksForCredentials, speakingForUrn);
        assert hasSpeaksForSupport();
    }

    public void setExtraOptionsForCallsWithCredential(@Nullable Map<String, ?> extraOptionsForCallsWithCredential) {
        this.extraOptionsForCallsWithCredential = extraOptionsForCallsWithCredential;
    }

    @Nullable
    protected Map<String, Object> addCredentialExtraOptions(@Nullable Map<String, Object> extraOptions) {
        if (extraOptionsForCallsWithCredential == null)
            return extraOptions;

        Map<String, Object> res = new HashMap<>();
        if (extraOptions != null) res.putAll(extraOptions);
        res.putAll(extraOptionsForCallsWithCredential);
        return res;
    }

    protected boolean hasUserCredentials(@Nullable List<AnyCredential> userCredentials) {
        if (userCredentials == null || userCredentials.isEmpty()) {
            return false; //has no valid credential
        }

        boolean hasUnknownCredential = false;
        boolean hasValidCredential = false;
        boolean hasExpiredCredential = false;
        List<AnyCredential> toRemove = new ArrayList<>();
        for (AnyCredential existingCredential : userCredentials) {
            if (existingCredential.getExpiresDate() == null) {
                LOG.warn("UserAndSliceApiWrapper could not determine an expirationDate for the current credential. We assume it is still valid.");
                hasUnknownCredential = true; //has a valid credential, with unknown expire time
            } else {
                //don't compare with now, compare with 2 minutes from now, since credential that expires soon is as good as useless.
                Date now = new Date(System.currentTimeMillis() + CREDENTIAL_EXPIRATION_MARGIN_IN_MS);
                if (existingCredential.getExpiresDate().before(now)) {
                    hasExpiredCredential = true; //has expired credential
                    LOG.info("UserAndSliceApiWrapper.hasUserCredentials encountered an expired credential");
                    toRemove.add(existingCredential);
                } else {
                    hasValidCredential = true; //has a valid credential
                }
            }
        }

        //remove expired just to be sure
        userCredentials.removeAll(toRemove);

        if (hasUnknownCredential && hasExpiredCredential)
            LOG.warn("UserAndSliceApiWrapper.hasUserCredentials encountered a credential with unknown expire time, and one with expired time. Will have to assume there IS a valid credential.");

        if (hasValidCredential) return true; //if at least one is valid and not expired, that's enough
        if (hasUnknownCredential)
            return true; //TODO because this is true, old credentials might be used. Just implement getExpiresDate for each credential type to make it work!
        if (hasExpiredCredential) return false;
        //has no valid credential at all
        return false;
    }

    /**
     * @return null or empty list if no credentials available
     */
    public abstract List<AnyCredential> getCachedUserCredentialsForAM();

    /**
     * get one or more credentials for the currently logged in user
     */
    public abstract List<AnyCredential> getUserCredentials(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException;

    /**
     * get one or more credentials for the given slice
     */
    public abstract List<AnyCredential> getSliceCredentials(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn) throws JFedException;

    /**
     * get the list of slices for the currently given user (typically the logged in user)
     */
    public abstract List<GeniUrn> getSlicesForUser(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException;

    /**
     * get info about the slivers the slice is running on
     */
    public abstract List<GeniUrn> getAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException;

    /**
     * get the slice expiration
     */
    public abstract Date getSliceExpiration(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException;

    public abstract boolean isRegisterAggregatesForSliceSupported();

    /**
     * @param logger               logger for call
     * @param sliceCredentials     credentials of the slice
     * @param sliceUrn             urn of the slice
     * @param aggregateUrn         aggregate on which the slivers exist (use multiple calls if slivers on multiple aggregates)
     * @param sliverUrns           list of slivers on same aggregate in same slice, with same expiration date
     * @param sliverExpirationDate expiration date of all slivers  (use multiple calls if slivers have different expiration dates)
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     */
    public void registerAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials,
                                           @Nonnull GeniUrn sliceUrn, @Nonnull GeniUrn aggregateUrn,
                                           @Nonnull Collection<GeniUrn> sliverUrns, @Nonnull Date sliverExpirationDate) throws JFedException {
        throw new UnsupportedOperationException("registerAggregatesForSlice is not supported by this API");
    }

    public void updateSliverExpirationDateInfoForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials,
                                                       @Nonnull GeniUrn sliceUrn, @Nonnull Collection<GeniUrn> sliverUrns,
                                                       @Nonnull Date sliverExpirationDate) throws JFedException {
        throw new UnsupportedOperationException("updateSliverExpirationDateInfoForSlice is not supported by this API");
    }

    /**
     * if sliverUnrs is null, this will try to unregister ALL slivers for the aggregate.
     *
     * @param sliverUrns List of sliverUrns to be unregistered. When null, all slivers of the slice will be unregistered
     */
    public void unregisterAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials,
                                             @Nonnull GeniUrn sliceUrn, @Nonnull GeniUrn aggregateUrn, @Nullable Collection<GeniUrn> sliverUrns) throws JFedException {
        throw new UnsupportedOperationException("unregisterAggregatesForSlice is not supported by this API");
    }

    /**
     * create a new slice with the given name
     */
    public SliceInfo createSlice(@Nonnull Logger logger, @Nonnull String sliceName, @Nullable String subAuthName) throws JFedException {
        return createSlice(logger, sliceName, null, subAuthName);
    }

    /**
     * create a new slice with the given name
     * <p/>
     * This call does not return anything!  (it is assumed this is used from the "HighLevelController", while you are processing the results asynchronously)
     */
    public abstract SliceInfo createSlice(@Nonnull Logger logger, @Nonnull String sliceName, @Nullable Date expirationDate, @Nullable String subAuthName) throws JFedException;

    /**
     * Renew the slice and set the expiration time to newExpirationDate
     *
     * @param sliceCredentials  Credentials of the slice to be renewed
     * @param newExpirationDate New Expiration date
     */
    public abstract List<AnyCredential> renewSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Date newExpirationDate) throws JFedException;

    /**
     * @param sliceCredentials list of credentials. Must contain at least one SfaCredential with a valid targetUrn
     * @return URN of the slice
     * @throws IllegalArgumentException When one of the SfaCredentials in sliceCredentials contains an invalid targetUrn, or when no SfaCredential is present
     */
    @Nonnull
    protected static GeniUrn findSliceUrn(@Nonnull List<AnyCredential> sliceCredentials) {
        for (AnyCredential c : sliceCredentials) {
            if (c instanceof SfaCredential) {
                SfaCredential sc = (SfaCredential) c;

                try {
                    final GeniUrn urn = new GeniUrn(sc.getTargetUrn());
                    if (Objects.equals(urn.getEncodedResourceType(), "slice")) {
                        return urn;
                    }
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new IllegalArgumentException("Credential contains an invalid targetUrn: '" + sc.getTargetUrn() + "'", e);
                }
            }
        }
        throw new IllegalArgumentException("No sliceUrn could be found in the given sliceCredentials");
    }

    /**
     * get all SSH keys for the given user (typically the logged in user)
     * <p/>
     * This call does not return anything!  (it is assumed this is used from the "HighLevelController", while you are processing the results asynchronously)
     */
    public abstract List<String> getSshKeysForUser(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException;

    public abstract SubAuthoritySupport getSubAuthoritySupport(@Nonnull Logger logger) throws JFedException;

    public abstract List<String> getSubAuthorityNames(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException;


    public abstract void shareSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn userUrn) throws JFedException;

    public abstract List<GeniUrn> getUsersForSubAuthority(@Nonnull Logger logger, @Nonnull String projectName) throws JFedException;

    public abstract List<GeniUrn> getUsersForSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException;

    public abstract void addUsersToSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull List<GeniUrn> userUrns, @Nonnull String role) throws JFedException;

    /*
         * sub authority support
         *`
         * Some servers/APIs require a subauthority, while others offer the option,a nd other forbid it.
         *
         * What this API hides:
        * For some API's (Protogeni SA),using a subauthority connects to a different URL
        * For others, it connects to the same URL
        * */
    public enum SubAuthoritySupport {
        SUB_AUTHORITY_MANDATORY, SUB_AUTHORITY_OPTIONAL, SUB_AUTHORITY_FORBIDDEN
    }

    public static class SliceInfo {
        @Nonnull
        private final String name;
        @Nonnull
        private final GeniUrn urn;
        @Nonnull
        private final List<AnyCredential> credentials;

        public SliceInfo(@Nonnull String name, @Nonnull GeniUrn urn, @Nonnull List<AnyCredential> credentials) {
            assert !credentials.isEmpty();

            this.name = name;
            this.urn = urn;
            this.credentials = new ArrayList<>(credentials);
        }

        @Nonnull
        public String getName() {
            return name;
        }

        @Nonnull
        public GeniUrn getUrn() {
            return urn;
        }

        @Nonnull
        public List<AnyCredential> getCredentials() {
            return credentials;
        }

        @Nonnull
        @Override
        public String toString() {
            return "SliceInfo{" +
                    "name='" + name + '\'' +
                    ", urn=" + urn +
                    ", credentials=" + credentials +
                    '}';
        }
    }
}
