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.*;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractGeniAggregateManager;
import be.iminds.ilabt.jfed.lowlevel.api.AggregateManager2;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.AggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.ErrorDetails;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.lowlevel.stitching.StitchingDirector;
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.*;

/**
 * AMv2Wrapper
 */
public class AMv2Wrapper extends AggregateManagerWrapper {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(AMv2Wrapper.class);
    private final AggregateManager2 am2;

    public AMv2Wrapper(Logger logger, GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider,
                       JFedPreferences jFedPreferences, Server amServer) {
        super(logger, geniUserProvider, connectionProvider, jFedPreferences, amServer);

        am2 = new AggregateManager2(logger, jFedPreferences);
    }


    protected SfaConnection getConnection() throws JFedException {
        return (SfaConnection) getConnection(new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 2));
    }

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

        //NOTE: speaksFor credential itself should always be supplied as part of argument list of credentials each option takes
        if (speakingForUrn != null)
            setExtraOptionsForCallsWithCredential(Collections.singletonMap("geni_speaking_for", speakingForUrn.getValue()));
    }

    @Override
    public Map getVersion() throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager2.VersionInfo> reply =
                am2.getVersion(getConnection());
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("GetVersion Call not successful", reply);
        }

        return reply.getRawResult();
    }

    @Override
    public String listResources(@Nonnull List<AnyCredential> userCredentials, boolean available, @Nonnull String type, @Nonnull String version) throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<String> reply = am2.listResources(
                getConnection(), finalizeSliceCredentials(userCredentials),
                type, version, available, true, null, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("ListResources Call not successful", reply);
        }

        return reply.getValue();
    }

    @Override
    public String createSliver(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String rspec, List<UserSpec> users, Date expirationDate)
            throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<String> reply = am2.createSliver(
                getConnection(),
                finalizeSliceCredentials(sliceCredentials),
                sliceUrn.getValue(), rspec,
                users, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (reply.getGeniResponseCode().isSuccess() && reply.getValue() == null) {
            throw new JFedException("CreateSliver Call has null reply value", reply);
        }
        if (reply.getGeniResponseCode().isSuccess() && !(reply.getValue() instanceof String)) {
            throw new JFedException("CreateSliver Call has invalid reply type: " + reply.getValue().getClass().getName(), reply);
        }

        String manifestRspec = reply.getValue();

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (AbstractGeniAggregateManager.isNotEnoughFreeResources(reply))
                throw new NotEnoughFreeResourcesException(reply);

            if (StitchingDirector.isAnyNotSupported(reply))
                throw new AnyVlanUnsupportedException(reply);

            if (StitchingDirector.isUnavailableVlan(reply))
                throw new VlanUnavailableException(StitchingDirector.getUnavailableVlan(reply), reply);

            boolean ignoreError = false;
            if ("Must delete existing slice first".equalsIgnoreCase(reply.getOutput())) {
                // If this occurs, the CreateSliver call has typically timed out but still succeeded.
                // The next call then succeeded but with a "Must delete existing slice first" error.
                // We can recover by requesting the manifest and returning it
                manifestRspec = describe(sliceUrn, sliceCredentials);
                ignoreError = true;
            }

            if (!ignoreError)
                throw new JFedException("CreateSliver Call not successful", reply);
        }

        if (expirationDate != null) {
            //AMv2 doesn't support setting the expirationDate on creation, so we now forcefully execute an renewSliver

            try {
                renewSlivers(sliceUrn, null, finalizeSliceCredentials(sliceCredentials), expirationDate);
            } catch (JFedException ex) {
                LOG.error("Error when trying to define expiration date for newly created sliver", ex);
            }
            lastAmReply = reply; //HACK set to last CreateSliver reply!
        }

        return manifestRspec;
    }

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

    @Override
    public AllocateReply allocate(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String rspec, Date expirationDate) throws JFedException {
        throw new RuntimeException("Unsupported operation");
    }

    @Override
    public AllocateReply allocate(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String rspec, Date startDate, Date endDate) throws JFedException {
        throw new UnsupportedOperationException("AMv2 does not support reservations");
    }

    @Override
    public String provisionAndStart(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, List<UserSpec> userSpecs, Date expirationDate) throws JFedException {
        throw new RuntimeException("Unsupported operation");
    }

    @Override
    public void deleteSliver(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<Boolean> reply = am2.deleteSliver(
                getConnection(),
                finalizeSliceCredentials(sliceCredentials),
                sliceUrn.getValue(), addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (Objects.equals(reply.getGeniResponseCode(), GeniAMResponseCode.GENIRESPONSE_SEARCHFAILED))
                return; //not an error, the sliver was already deleted, so all is ok.

            throw new JFedException("Delete Call not successful", reply);
        }
    }

    @Override
    public StatusDetails status(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        LOG.debug("Calling AMv2Wrapper status with " + sliceCredentials.size() + " credentials");
        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager2.SliverStatus> reply = am2.sliverStatus(
                getConnection(), finalizeSliceCredentials(sliceCredentials),
                sliceUrn.getValue(), addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (reply.getGeniResponseCode() == GeniAMResponseCode.GENIRESPONSE_SEARCHFAILED) {
            //the AM could not find any slivers associated to the given slice
            return new StatusDetails(StatusDetails.SliverStatus.UNALLOCATED);
        }

        AggregateManager2.SliverStatus status = reply.getValue();

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("SliverStatus Call not successful", reply);
        }
        if (status == null) {
            throw new JFedException("SliverStatus Call has no return value", reply);
        }

        if (Objects.equals(status.getStatus(), "ready") && status.getResources().isEmpty()) {
            throw new JFedException("SliverStatus Call returned \"ready\", but there are no resources. This is probably a server error.", reply);
        }

        //convert AggregateManager2.SliverStatus to SliverStatusDetails
        StatusDetails res = new StatusDetails(status.getStatus());
        res.addKnownExpires(status.getAnyExtExpires());

        for (AggregateManager2.SliverStatus.ResourceStatus resourceStatus : status.getResources()) {
            if (resourceStatus.getError() != null && !resourceStatus.getError().isEmpty()) {
                res.addError(new ErrorDetails(resourceStatus.getError(), null, resourceStatus.getUrn()));
            }
            res.addSliverStatus(resourceStatus.getUrn(), resourceStatus.getStatus());
        }


        return res;
    }

    @Override
    public String describe(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        String type = "geni";
        final String version = "3";

        //FEDIBBTDEV-1392: workaround for bug at al2s (it should ignore case but doesn't)
        if (amServer != null && amServer.getServerType() != null
                && amServer.getServerType().equalsIgnoreCase("FOAM"))
            type = "GENI";

        AbstractGeniAggregateManager.AggregateManagerReply<String> reply = am2.listResources(
                getConnection(), finalizeSliceCredentials(sliceCredentials),
                type, version, null, true, sliceUrn.getValue(), addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (reply.getGeniResponseCode() == GeniAMResponseCode.GENIRESPONSE_SEARCHFAILED) {
                //the AM could not find any slivers associated to the given slice
                throw new JFedException("No such slice found", reply);
            }

            throw new JFedException("ListResources Call not successful", reply);
        }

        return reply.getValue();
    }

    @Override
    public void renewSlivers(GeniUrn sliceUrn, List<GeniUrn> sliverUrns, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Date newExpire) throws JFedException {
        if (sliceUrn == null)
            throw new JFedException("RenewSliver not called, because sliceUrn == null and this call supports only slice URN. (sliverUrns=" + sliverUrns + ")");

        AbstractGeniAggregateManager.AggregateManagerReply<Boolean> reply = am2.renewSliver(
                getConnection(), finalizeSliceCredentials(sliceCredentials),
                sliceUrn.getValue(), newExpire, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("RenewSliver Call not successful", reply);
        }
    }

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

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

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

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

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

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

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

}
