package be.iminds.ilabt.jfed.ui.javafx.probe_gui.credential_manipulation;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.controller.Task;
import be.iminds.ilabt.jfed.highlevel.controller.TaskExecution;
import be.iminds.ilabt.jfed.highlevel.controller.TaskThread;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.tasks.HighLevelTaskFactory;
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.ProtogeniSliceAuthority;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
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.JavaFXLogger;
import javafx.application.Platform;
import javafx.util.Pair;
import org.slf4j.LoggerFactory;

import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.CountDownLatch;

/**
 * CredentialCreatorPanel
 */
public abstract class CredentialCreatorPanel {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(CredentialCreatorPanel.class);


    private final JavaFXLogger logger;
    private final AuthorityFinder authorityFinder;
    protected final GeniUserProvider geniUserProvider;
    protected final JFedConnectionProvider connectionProvider;
    private final SfaModel sfaModel;
    private final HighLevelTaskFactory hltf;
    private final TaskThread tt;
    private final JFedPreferences jFedPreferences;

    private final Map<GeniUrn, X509Certificate> resolvedUserCredentials = new HashMap<>();


    protected CredentialCreatorPanel(JavaFXLogger logger, AuthorityFinder authorityFinder,
                                     GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider,
                                     SfaModel sfaModel, HighLevelTaskFactory hltf,
                                     TaskThread tt, JFedPreferences jFedPreferences) {
        super();
        this.logger = logger;
        this.authorityFinder = authorityFinder;
        this.geniUserProvider = geniUserProvider;
        this.connectionProvider = connectionProvider;
        this.sfaModel = sfaModel;
        this.hltf = hltf;
        this.tt = tt;
        this.jFedPreferences = jFedPreferences;
    }


    protected Task resolveUser(final GeniUrn otherUserUrn) {
        final Server otherUserAuth =
                authorityFinder.findByAnyUrn(otherUserUrn.getValue(), AuthorityFinder.Purpose.FIND_USERAUTH);

        Task resolveUserCall = new Task("Resolve User " + otherUserUrn) {
            @Override
            public void doTask(TaskExecution singleTask) throws JFedException, InterruptedException {
                LOG.debug("Resolving target to get certificate");

                //TODO: use wrapper
//                UserAndSliceApiWrapper wrapper = new AutomaticUserAndSliceApiWrapper(guiModel.getLogger(), guiModel.getGeniUserProvider(), guiModel.getConnectionProvider());
//                UserInfo userInfo = wrapper.getUserInfo(otherUserUrn)

                SfaConnection con = (SfaConnection) connectionProvider.getConnectionByAuthority(
                        geniUserProvider.getLoggedInGeniUser(),    //try to log in as local user. Might not work at all off course if this is different authority
                        otherUserAuth,
                        new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_SA, 1));
                ProtogeniSliceAuthority sa = new ProtogeniSliceAuthority(logger, jFedPreferences);

                List<AnyCredential> userCreds = sfaModel.getUserCredentials();
                assert userCreds != null && !userCreds.isEmpty();
                ProtogeniSliceAuthority.SliceAuthorityReply<ProtogeniSliceAuthority.UserInfo> reply =
                        sa.resolveUser(con, AnyCredential.toSingleCredential(userCreds), otherUserUrn);

                if (!reply.getGeniResponseCode().isSuccess())
                    return;
                if (reply.getValue().getCertificate() == null) {
                    LOG.warn("Reply was succesful, but no certificate was returned");
                    return;
                }
                resolvedUserCredentials.put(otherUserUrn, reply.getValue().getCertificate());
            }

            @Override
            public List<Task> initDependsOn() {
                List<Task> res = new ArrayList<>();
                res.add(hltf.getUserCredential());
                return res;
            }
        };
        return resolveUserCall;
    }

    protected Pair<GeniUrn, X509Certificate> getUserInfo(String urnString) throws GeniUrn.GeniUrnParseException, JFedException {
        GeniUrn otherUserUrn = new GeniUrn(urnString);

        X509Certificate cert;
        if (Objects.equals(geniUserProvider.getLoggedInGeniUser().getUserUrnString(), otherUserUrn.getValue())) {
            LOG.debug("getUserInfo() User is logged in user: all information already known for " + otherUserUrn.getValue());
            cert = geniUserProvider.getLoggedInGeniUser().getClientCertificateChain().get(0); //TODO handle if not first
        } else {
            LOG.debug("getUserInfo() Getting needed information using ProtoGeni SA for urn: " + otherUserUrn.getValue());
            //TODO support more than just protogeni slice authority

            if (!resolvedUserCredentials.containsKey(otherUserUrn)) {
                LOG.trace("getUserInfo() Fetching user info");
                Task task = resolveUser(otherUserUrn);
                CountDownLatch latch = task.getCompletionCountDownLatch();
                tt.addTask(task);
                try {
                    assert !Platform.isFxApplicationThread(); //should not wait on JavaFX thread, will lock up as JavaFXLogger has thread that waits for Platform.runLater to occur!

                    LOG.trace("getUserInfo() waiting for task completion");
                    latch.await();
                    LOG.trace("getUserInfo() task completion");
                } catch (InterruptedException e) {
                    LOG.warn("CountDownLatch InterruptedException", e);
                }

                cert = resolvedUserCredentials.get(otherUserUrn);
            } else
                cert = resolvedUserCredentials.get(otherUserUrn);
        }
        LOG.trace("getUserInfo() return");

        return new Pair<>(otherUserUrn, cert);
    }


    protected String findUrnInCertificate(X509Certificate cert) {
        Collection<List<?>> altNames = null;
        try {
            altNames = cert.getSubjectAlternativeNames();
        } catch (CertificateParsingException e) {
            LOG.error("Error processing certificate alternate names: " + e.getMessage());
            return null;
        }

        String userAuth = null;

        LOG.trace("certificate has alt names: " + (altNames != null && altNames.isEmpty()) + "\n");

        if (altNames != null) {
            String invalidUrnErrors = "";
            boolean hasvalidUrn = false;
            for (List<?> altName : altNames) {
                Integer nameType = (Integer) altName.get(0);
                LOG.trace("certificate has altname of type " + nameType + "\n");
                if (nameType == 6) {
                    //uniformResourceIdentifier
                    String urn = (String) altName.get(1);
                    GeniUrn geniUrn = GeniUrn.parse(urn);
                    if (geniUrn == null || !Objects.equals(geniUrn.getEncodedResourceType(), "user"))
                        invalidUrnErrors +=
                                "Warning: certificate alternative name URI is not a valid user urn: \"" + urn + "\"  (will be ignored)";
                    else {
                        hasvalidUrn = true;
                        userAuth = geniUrn.getEncodedTopLevelAuthority();
                        String userName = geniUrn.getEncodedResourceName();
                        String userUrn = urn;
                        LOG.trace("processed altName of URN type. userUrn=" + userUrn + "\n");
                        return userUrn;
                    }
                }
            }
            if (!hasvalidUrn && !invalidUrnErrors.isEmpty()) {
                LOG.warn(invalidUrnErrors);
            }
        }

        LOG.warn("no urn found in certificate: " + cert);
        setResult("ERROR: no urn found in certificate: " + cert);
        return null;
    }

    protected abstract void setResult(final String text);

    protected X509Certificate certificateRetriever(String targetUrn) {
        LOG.trace("certificateRetriever() getting target info for urn " + targetUrn);
        Pair<GeniUrn, X509Certificate> targetInfo = null;
        try {
            targetInfo = getUserInfo(targetUrn);
            if (targetInfo == null || targetInfo.getValue() == null) {
                setResult("ERROR getting certificate for urn: " + targetUrn);
                return null;
            }
            return targetInfo.getValue();
        } catch (GeniUrn.GeniUrnParseException e) {
            LOG.error("ERROR urn is invalid: " + targetUrn, e);
            setResult("ERROR urn is invalid: " + targetUrn);
            return null;
        } catch (JFedException e) {
            LOG.error("Error in call retrieving info for urn: " + targetUrn, e);
            setResult("Error in call retrieving info for urn: " + targetUrn);
            return null;
        }
    }
}
