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

import be.iminds.ilabt.jfed.lowlevel.Gid;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.ServerType;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.LoggerFactory;

import java.net.URL;
import java.util.*;

/**
 * AuthorityListModel is a list of GeniAuthority
 * It can be empty.
 * <p/>
 * Authorities can be added and removed, or "merged".
 * "merging" authorities is useful when there are 2 sources of data about an authority.
 * The data can then be combined.
 * <p/>
 * You can listen for changes in the list.
 */
@SuppressWarnings("deprecation")
@Deprecated
public class LegacyAuthorityListModel {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LegacyAuthorityListModel.class);
    private final List<LegacySfaAuthority> authorities;
    private final List<AuthorityListModelListener> changeListeners = new ArrayList<>();

    private Boolean forceExoSM = null; //null means "no exception for exogeni", so do what testbeds.xml tells

    //was package scope, so that only JFedAuthorityList can create a new AuthorityListModel
    //however, the JavaFX gui needs to have an external list
    public LegacyAuthorityListModel() {
        authorities = new ArrayList<>();
    }

    public List<LegacySfaAuthority> getAuthorities() {
//        assert authorities.size() > 0;
        assert !authorities.contains(null);
        return Collections.unmodifiableList(authorities);
    }

    public int size() {
        return authorities.size();
    }


//    public SfaAuthorityBackwardCompatibility getByUrn(GeniUrn urn, SubAuthMatchingMode matchingMode) {
//        return getByUrn(urn, matchingMode, forceExoSM);
//    }
//    public SfaAuthorityBackwardCompatibility getByUrn(GeniUrn urn, SubAuthMatchingMode matchingMode, @Nonnull ExogeniChoice exogeniChoice) {
//        return getByUrn(urn.toString(), matchingMode, forceExoSM);
//    }
//
//    private static boolean exogeniForceSM(String topTld, String subAuth, @Nonnull ExogeniChoice exogeniChoice) {
//        if (exogeniChoice == ExogeniChoice.USE_GLOBAL_SETTING) return false;
//        if (!topTld.equals("exogeni.net"))
//            return false;
//        return forceExoSM;
//    }
//    private static boolean exogeniForceNonSM(String topTld, String subAuth, @Nonnull ExogeniChoice exogeniChoice) {
//        if (exogeniChoice == ExogeniChoice.USE_GLOBAL_SETTING) return false;
//        if (!topTld.equals("exogeni.net"))
//            return false;
//        return !forceExoSM;
//    }
//
//    public void setForceExoSM(Boolean forceExoSM) {
//        this.forceExoSM = forceExoSM;
//    }

    public enum SubAuthMatchAllowed {
        ALLOW_ONLY_EXACT_SUBAUTHORITY, /*Sub authorities (or lack of sub authority) must be identical */
        ALLOW_TOPLEVEL, /* Sub authority either identical, or there exists an authority that is the same but without subauthority.
                                    (includes same matches as ALLOW_ONLY_EXACT_SUBAUTHORITY) */
        ALLOW_OTHER_SUBAUTHORITY /* match all with same authority, regardless of no or different sub authority.
                                    (includes same matches as ALLOW_OTHER_SUBAUTHORITY and ALLOW_ONLY_EXACT_SUBAUTHORITY) */
    }

    public enum SubAuthMatchPreference {
        PREFER_EXACT_SUBAUTHORITY /*Sub authorities (or lack of sub authority) must be identical */,
        PREFER_TOPLEVEL /* Returns authority without subauth, or if that is not found, the authority with sub authority will be returned if found. */,
        /* if ALLOW_OTHER_SUBAUTHORITY, an auth with other subauthority will only be returned when there is no matching toplevel or subauth athority. */
        /* if an urn without sub auth is given, both choices prefer the toplevel (as PREFER_EXACT_SUBAUTHORITY then prefers toplevel)*/
    }

    /**
     * Retreive a GeniAuthority by URN.
     *
     * @param urn            the URN of the GeniAuthority to retreive
     * @param matchAllowed   which matches are allowed
     * @param matchPreferred if there are multiple matches, which is preferred?
     * @return the GeniAuthority by URN. If no authority is known for the URN, return null
     */
    public LegacySfaAuthority getByUrn(final GeniUrn urn, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred, boolean ignoreUrnTypeAndName) {
        LOG.trace("AuthorityListModel.getByUrn(" + urn + ", " + matchAllowed + ", " + matchPreferred + ", " + ignoreUrnTypeAndName + ")");

        if (!ignoreUrnTypeAndName && !Objects.equals(urn.getResourceType(), "authority")) {
            LOG.warn("Tried to lookup SfaAuthorityBackwardCompatibility with non-authority URN \"" + urn + "\". It will likely not be found in list of authority URNs");
            assert false;
        }

        List<LegacySfaAuthority> exactMatches = new ArrayList<>();
        List<LegacySfaAuthority> topLevelMatches = new ArrayList<>();
        List<LegacySfaAuthority> ignoreSubAuthMatches = new ArrayList<>();

        int urnSubAuthCount = urn.getSubAuthCount();

        for (LegacySfaAuthority auth : authorities) {
            GeniUrn authUrn = auth.getUrn();
            int authSubAuthCount = authUrn.getSubAuthCount();

            if (!ignoreUrnTypeAndName) {
                if (!Objects.equals(authUrn.getResourceType(), urn.getResourceType()))
                    continue;
                if (!Objects.equals(authUrn.getResourceName(), urn.getResourceName()))
                    continue;
            }

            //if TLD doesn't match, it won't ever match
            if (!Objects.equals(authUrn.getTopLevelAuthority_withoutSubAuth(), urn.getTopLevelAuthority_withoutSubAuth()))
                continue;

            //first search for exact auth and sub auth matches
            if (Objects.equals(authUrn.getTopLevelAuthority(), urn.getTopLevelAuthority())) {
                exactMatches.add(auth);
            }

            //search again, and consider authorities without less sub authorities (= more top level), that are the same
            if (!authUrn.hasSubAuthority())
                topLevelMatches.add(auth); //matches, since TLD matches
            if (authSubAuthCount > 0 && authSubAuthCount < urnSubAuthCount && Objects.equals(urn.getSubAuthNames(authSubAuthCount), authUrn.getSubAuthName()))
                topLevelMatches.add(auth);

            //finally, search again ignoring sub authorities
            ignoreSubAuthMatches.add(auth); //matches, since TLD matches
        }

        switch (matchAllowed) {
            case ALLOW_ONLY_EXACT_SUBAUTHORITY: {
                topLevelMatches.clear();
                ignoreSubAuthMatches.clear();
                break;
            }
            case ALLOW_TOPLEVEL: {
                ignoreSubAuthMatches.clear();
                break;
            }
            case ALLOW_OTHER_SUBAUTHORITY:
                break;
        }

        switch (matchPreferred) {
            case PREFER_EXACT_SUBAUTHORITY: {
                for (LegacySfaAuthority auth : exactMatches)
                    return auth;
                for (LegacySfaAuthority auth : sortBySubAuthDifference(topLevelMatches, urn))
                    return auth;
                for (LegacySfaAuthority auth : sortBySubAuthDifference(ignoreSubAuthMatches, urn))
                    return auth;
                return null;
            }
            case PREFER_TOPLEVEL: {
                for (LegacySfaAuthority auth : sortBySubAuthNumber(topLevelMatches))
                    return auth;
                for (LegacySfaAuthority auth : exactMatches)
                    return auth;
                for (LegacySfaAuthority auth : sortBySubAuthNumber(ignoreSubAuthMatches))
                    return auth;
                return null;
            }
            default:
                throw new RuntimeException("match preference not implemented: " + matchPreferred);
        }
    }

    /**
     * sort by distance from number of subauths of provided urn. The first has the closest possible subauth to the provided urn.
     */
    static List<LegacySfaAuthority> sortBySubAuthDifference(List<LegacySfaAuthority> l, final GeniUrn urn) {
        l.sort(Comparator.comparing(auth -> getDistance(urn, auth)));
        return l;
    }

    private static int getDistance(GeniUrn urn, LegacySfaAuthority a) {
        return Math.abs(a.getUrn().getSubAuthCount() - urn.getSubAuthCount());
    }

    /**
     * Sort by number of sub auths. With smallest number of sub auths first.
     */
    static List<LegacySfaAuthority> sortBySubAuthNumber(List<LegacySfaAuthority> l) {
        l.sort(Comparator.comparing(auth -> auth.getUrn().getSubAuthCount()));
        return l;
    }

    public LegacySfaAuthority getByUrn(String urnString, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred, boolean ignoreUrnTypeAndName) {
        GeniUrn u = GeniUrn.parse(urnString);
        if (u == null) {
            LOG.warn("Tried to lookup SfaAuthorityBackwardCompatibility using the invalid URN \"" + urnString + "\" -> solving problem by returning null");
            return null;
        }
        return getByUrn(u, matchAllowed, matchPreferred, ignoreUrnTypeAndName);
    }

    public LegacySfaAuthority getByUrn(String urnString, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred) {
        return getByUrn(urnString, matchAllowed, matchPreferred, false/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getByUrn(GeniUrn urn, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred) {
        return getByUrn(urn, matchAllowed, matchPreferred, false/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getByUrnExact(String urnString) {
        return getByUrn(urnString, LegacyAuthorityListModel.SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, LegacyAuthorityListModel.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY, false/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getByUrnExact(GeniUrn urn) {
        return getByUrn(urn, LegacyAuthorityListModel.SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, LegacyAuthorityListModel.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY, false/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getFromAnyUrn(String urnString, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred) {
        return getByUrn(urnString, matchAllowed, matchPreferred, true/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getFromAnyUrn(GeniUrn urn, SubAuthMatchAllowed matchAllowed, SubAuthMatchPreference matchPreferred) {
        return getByUrn(urn, matchAllowed, matchPreferred, true/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getFromAnyUrnExact(String urnString) {
        return getByUrn(urnString, LegacyAuthorityListModel.SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, LegacyAuthorityListModel.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY, true/*ignoreUrnTypeAndName*/);
    }

    public LegacySfaAuthority getFromAnyUrnExact(GeniUrn urn) {
        return getByUrn(urn, LegacyAuthorityListModel.SubAuthMatchAllowed.ALLOW_ONLY_EXACT_SUBAUTHORITY, LegacyAuthorityListModel.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY, true/*ignoreUrnTypeAndName*/);
    }


    public LegacySfaAuthority getByUrl(URL scsUrl) {
        for (LegacySfaAuthority auth : authorities) {
            if (auth.getUrls().values().contains(scsUrl))
                return auth;
        }
        return null;
    }


    public boolean removeByUrn(String urn) {
        for (LegacySfaAuthority auth : authorities) {
            if (Objects.equals(auth.getUrnString(), urn)) {
                authorities.remove(auth);
                fireChange();
                return true;
            }
        }
        return false;
    }

    void removeAll() {
        if (authorities.isEmpty()) return;
//        List<SfaAuthorityBackwardCompatibility> allAuth = new ArrayList<SfaAuthorityBackwardCompatibility>(authorities);
//        for (SfaAuthorityBackwardCompatibility auth : allAuth)
//            authorities.remove(auth);
        authorities.clear();
        fireChange();
    }

    /**
     * Does not call fireChange. do this manually afterwards!
     */
    public LegacySfaAuthority createOrUpdate(String urn, String name, Map<ServerType, URL> urls, List<LegacySfaAuthority.ProxyInfo> proxies, Gid gid, String type) {
        for (LegacySfaAuthority auth : authorities) {
            if (Objects.equals(auth.getUrnString(), urn)) {
                auth.updateAll(name, urls, gid, type);
                return auth;
            }
        }
        try {
            LegacySfaAuthority newAuth = new LegacySfaAuthority(urn, name, null, urls, proxies, gid, type);
            authorities.add(newAuth);
            return newAuth;
        } catch (JFedException e) {
            LOG.warn("WARNING: Error adding authority (=> authority not added!): \"" + urn + "\": " + e.getMessage(), e);
            return null;
        }
    }

    /**
     * Does not call fireChange. do this manually afterwards!
     */
    public LegacySfaAuthority mergeOrAdd(LegacySfaAuthority auth) {
        for (LegacySfaAuthority existingAuth : authorities) {
            if (Objects.equals(existingAuth.getUrnString(), auth.getUrnString())) {
                existingAuth.updateAll(auth.getName(), auth.getUrls(), auth.getGid(), auth.getType());
                return existingAuth;
            }
        }
        authorities.add(auth);
        return auth;
    }

    public void addAuthority(LegacySfaAuthority auth) {
        assert auth != null;
        assert auth.getUrn() != null;
        assert auth.getUrnString() != null;
        assert !authorities.contains(auth) : "this already contains SfaAuthorityBackwardCompatibility with urn "+auth.getUrnString();
        this.authorities.add(auth);
        fireChange();
    }

    public void fireChange() {
        changeListeners.forEach(AuthorityListModelListener::onAuthorityListChanged);
    }

    public void addAuthorityListModelListener(AuthorityListModelListener l) {
        changeListeners.add(l);
    }

    public void removeAuthorityListModelListener(AuthorityListModelListener l) {
        changeListeners.remove(l);
    }

    public interface AuthorityListModelListener {
        //        public void onAuthorityAdded();
        void onAuthorityListChanged();
    }
}
