package be.iminds.ilabt.jfed.highlevel.model;

import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.GeniUserWithSpeaksFor;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.rspec.model.ModelRspec;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecFactory;
import be.iminds.ilabt.jfed.rspec.parser.RspecParseException;
import be.iminds.ilabt.jfed.rspec.parser.StaxRspecParser;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.KeyUtil;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.slf4j.LoggerFactory;

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

/**
 * UserSpecHelper
 * <p/>
 * get UserSpec list from:
 * - logged in user
 * - rspec extension  (http://jfed.iminds.be/rspec/ext/jfed-ssh-keys/1)
 * - combination of both
 */
public class UserSpecHelper {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(UserSpecHelper.class);

    public static List<UserSpec> getUserUserSpecList(GeniUser geniUser) {
        List<UserSpec> userSpecs = new ArrayList<>();
        List<String> keys = new ArrayList<>();
        keys.add(KeyUtil.publicKeyToOpenSshAuthorizedKeysFormat(geniUser.getPublicKey()));
        UserSpec user = new UserSpec(geniUser.getUserUrnString(), keys);
        userSpecs.add(user);
        return userSpecs;
    }

    public static List<UserSpec> getUserUserSpecList(GeniUserProvider geniUserProvider, SfaModel sfaModel) {
        List<UserSpec> userSpecs = new ArrayList<>();
        UserSpec user = getUserUserSpec(geniUserProvider, sfaModel);
        if (user.getSshKey().isEmpty()) {
            GeniUser geniUser = geniUserProvider.getLoggedInGeniUser();
            GeniUserWithSpeaksFor geniUserWithSpeaksFor =
                    (geniUser instanceof GeniUserWithSpeaksFor) ? (GeniUserWithSpeaksFor) geniUser : null;
            if (geniUserWithSpeaksFor != null) {
                user.getSshKey().add(KeyUtil.publicKeyToOpenSshAuthorizedKeysFormat(geniUserWithSpeaksFor.getSpeaksForUserPublicKey()));
            } else {
                user.getSshKey().add(KeyUtil.publicKeyToOpenSshAuthorizedKeysFormat(geniUserProvider.getLoggedInGeniUser().getPublicKey()));
            }
        }
        userSpecs.add(user);
        return userSpecs;
    }

    public static UserSpec getUserUserSpec(GeniUserProvider geniUserProvider, SfaModel sfaModel) {
        GeniUser geniUser = geniUserProvider.getLoggedInGeniUser();
        GeniUserWithSpeaksFor geniUserWithSpeaksFor =
                (geniUser instanceof GeniUserWithSpeaksFor) ? (GeniUserWithSpeaksFor) geniUser : null;
        if (geniUserWithSpeaksFor != null) {
            return new UserSpec(geniUserWithSpeaksFor.getSpeaksForUrn().toString(), sfaModel.getUserKeys(geniUserWithSpeaksFor.getSpeaksForUrn()));
        } else {
            return new UserSpec(geniUser.getUserUrnString(), sfaModel.getUserKeys(geniUser.getUserUrn()));
        }
    }

    public static List<UserSpec> getUserSpecFromRspecExt(String rspec) {
        try {
            ModelRspec modelRspec = new StaxRspecParser(rspec, FXRspecFactory.getInstance()).call();
            LOG.debug("getUserSpecFromRspecExt model with reconstructed modelRspec.getSshKeys().size()=" + modelRspec.getSshKeys().size());
            return new ArrayList<>(modelRspec.getSshKeys());
        } catch (RspecParseException e) {
            LOG.debug("getUserSpecFromRspecExt error parsing", e);
            return new ArrayList<>();
        }
    }

    public static List<UserSpec> getUserSpecFromRspecExt(ModelRspec modelRspec) {
        LOG.debug("getUserSpecFromRspecExt model with modelRspec.getSshKeys().size()=" + modelRspec.getSshKeys().size());
        return new ArrayList<>(modelRspec.getSshKeys());
    }

    public static List<UserSpec> getMergedUserSpecs(GeniUserProvider geniUserProvider, SfaModel sfaModel, String rspec) {
        return getMergedUserSpecs(geniUserProvider, sfaModel, getUserSpecFromRspecExt(rspec));

    }

    public static List<UserSpec> getMergedUserSpecs(GeniUserProvider geniUserProvider, SfaModel sfaModel, ModelRspec modelRspec) {
        return getMergedUserSpecs(geniUserProvider, sfaModel, getUserSpecFromRspecExt(modelRspec));
    }

    private static List<UserSpec> getMergedUserSpecs(GeniUserProvider geniUserProvider, SfaModel sfaModel, List<UserSpec> rspecUserSpecs) {
        List<UserSpec> userUserSpecs = getUserUserSpecList(geniUserProvider, sfaModel);

        Map<String, UserSpec> userToSpec = new HashMap<>();
        for (UserSpec u : userUserSpecs)
            userToSpec.put(u.getUrn(), u);
        Set<String> userSpecUsers = userToSpec.keySet();

        LOG.debug("userUserSpecs=" + userUserSpecs);
        LOG.debug("rspecUserSpecs=" + rspecUserSpecs);

        Set<String> rspecUsers = new HashSet<>();
        List<UserSpec> res = new ArrayList<>();
        for (UserSpec rspecUserSpec : rspecUserSpecs) {
            boolean defaultUser = rspecUserSpec.getUrn() == null || rspecUserSpec.getUrn().trim().isEmpty();
            //note: empty user urn in RSpec ext means: add keys to logged in user
            if (defaultUser || userSpecUsers.contains(rspecUserSpec.getUrn())) {
                assert !userSpecUsers.isEmpty();
                String rspecUserSpecUrn = defaultUser ? userSpecUsers.iterator().next() : rspecUserSpec.getUrn();
                assert rspecUserSpecUrn != null;

                UserSpec userSpecFromUser = userToSpec.get(rspecUserSpecUrn);
                Set<String> allKeys = new HashSet<>(userSpecFromUser.getSshKey());
                allKeys.addAll(rspecUserSpec.getSshKey());
                res.add(new UserSpec(rspecUserSpecUrn, allKeys));
                rspecUsers.add(rspecUserSpecUrn);
            } else {
                res.add(rspecUserSpec);
                rspecUsers.add(rspecUserSpec.getUrn());
            }
        }
        userSpecUsers.removeAll(rspecUsers);
        res.addAll(userSpecUsers.stream().map(userToSpec::get).collect(Collectors.toList()));
        return res;
    }

    public static List<UserSpec> getProjectMemberUserSpecs(String projectName, SfaModel sfaModel) {
        List<UserSpec> res = new ArrayList<>();
        Collection<GeniUrn> userUrns = sfaModel.getUsersBySubAuthority(projectName);
        for (GeniUrn userUrn : userUrns) {
            Collection<String> keys = sfaModel.getUserKeys(userUrn);
            res.add(new UserSpec(userUrn.toString(), keys));
        }
        return res;
    }

    public static List<UserSpec> getSliceMemberUserSpecs(@Nonnull GeniUrn sliceUrn, @Nonnull SfaModel sfaModel) {
        List<UserSpec> res = new ArrayList<>();
        Slice slice = sfaModel.getSlice(sliceUrn);
        if (slice == null)
            return Collections.emptyList();
        Collection<GeniUrn> userUrns = slice.getUsers();
        for (GeniUrn userUrn : userUrns) {
            Collection<String> keys = sfaModel.getUserKeys(userUrn);
            res.add(new UserSpec(userUrn.toString(), keys));
        }
        return res;
    }

    public static List<UserSpec> mergeUserSpecs(@Nullable Collection<UserSpec> a, @Nullable Collection<UserSpec> b) {

        Multimap<String, String> userSpecMap = HashMultimap.create();

        if (a != null)
            addAndMergeUserSpecMap(userSpecMap, a);
        if (b != null)
            addAndMergeUserSpecMap(userSpecMap, b);

        return new ArrayList<>(userSpecMap.asMap().entrySet().stream()
                .map(userSpec -> new UserSpec(userSpec.getKey(), userSpec.getValue()))
                .collect(Collectors.toList()));

    }

    public static List<UserSpec> addUserSpec(Collection<UserSpec> userSpecs, UserSpec userSpec) {
        return mergeUserSpecs(userSpecs, Collections.singleton(userSpec));
    }

    private static void addAndMergeUserSpecMap(Multimap<String, String> target, Collection<UserSpec> input) {
        for (UserSpec userSpec : input) {
            for (String key : userSpec.getSshKey()) {
                target.put(userSpec.getUrn(), trimSshKey(key));
            }
        }
    }

    public static String trimSshKey(String input) {
        return input.replace("\n", "").replace("\r", "").trim();
    }
}
