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.lowlevel.authority.binding.Authorities;
import be.iminds.ilabt.jfed.lowlevel.stitching.VlanRange;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.KeyUtil;
import org.slf4j.LoggerFactory;

import javax.xml.bind.*;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.stream.Collectors;

/**
 * SfaAuthorityBackwardCompatibility is a mutable class.
 * The URN never changes, and is the only thing used for testing equality: we always know at least the URN, but all
 * other info may be unknown initially and can be updated later on.
 * <p/>
 * A lot of getter's will always return something non null, even if there is nothing known yet. They derive info
 * from other sources, like the URN to do so.
 * <p/>
 * <p/>
 * This class represents an "authority", but that is a vague term here.
 * It contains info about one "site", which typically has it's own aggregate manager,
 * and often also it's own identity/slice authority/registry,
 * <p/>
 * This class keeps basic info such as URL's, urn, etc...
 */
@SuppressWarnings("deprecation")
@Deprecated
public class LegacySfaAuthority {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(LegacySfaAuthority.class);

    private static final String FAKE_TYPE = "fake";
    public static final boolean debug = true; //ugly hack
    private GeniUrn urn;
    private String urnString;
    private String urnPart; //name if part of urn    ex: wall3.test.ibbt.be
    private String hrn;
    private Location location;
    private Map<ServerType, URL> urls;
    private List<ProxyInfo> proxies;
    private Gid gid;
    /**
     * Example types: emulab, planetlab
     */
    private String type;
    private InfoSource source; //the source this authority info comes from;
    private boolean wasStored = false; //local storage is not a source, as it needs to store source info...

    /**
     * List of certificates that need to be trusted.
     * <p/>
     * There can be multiple certificates which have to be trusted. (typical use: 2 certificates, the old in use now, and the new one that is not yet in use, but will soon be.)
     * Each element (String) can contain multiple certificates as well. (typical use: certificate chain)
     * <p/>
     * (a chain may optionally be added as multiple certificates)
     */
    private List<String> pemSslTrustCerts;
    /**
     * Sometimes, the SSL cerificate for a host does not have the correct name for that host. For example, if the url is:
     * https://sfa.planet-lab.eu:12347, while the certificate is for "ple". To fix these issues, add allowed aliases for
     * the certificate here. In the example, you would add "ple" to this list.
     */
    private List<String> allowedCertificateHostnameAliases;

    private GeniUrn defaultScsUrn;


    private final Set<SpecialCase> specialCases = new HashSet<>();

    /**
     * copy constructor
     */
    public LegacySfaAuthority(LegacySfaAuthority otherAuthority) {
        this.urn = otherAuthority.urn;
        this.urnString = otherAuthority.urnString;
        this.urnPart = otherAuthority.urnPart;
        this.hrn = otherAuthority.hrn;
        this.location = otherAuthority.location;
        if (otherAuthority.gid != null)
            this.gid = new Gid(otherAuthority.gid);
        else
            this.gid = null;
        this.urls = new HashMap<>(otherAuthority.urls);
        this.proxies = new ArrayList<>(otherAuthority.proxies);
        this.source = otherAuthority.source;
        this.pemSslTrustCerts =
                otherAuthority.pemSslTrustCerts == null ? null : new ArrayList<>(otherAuthority.pemSslTrustCerts);
        this.allowedCertificateHostnameAliases =
                new ArrayList<>(otherAuthority.allowedCertificateHostnameAliases);
        this.defaultScsUrn = otherAuthority.defaultScsUrn;
        this.specialCases.addAll(otherAuthority.specialCases);
    }

    /**
     * @param urnString the URN (may never be null)
     * @param hrn       may be null
     * @param location  may be null
     * @param urls      may be null
     * @param proxies   may be null
     * @param gid       may be null
     * @param type      may be null
     */
    public LegacySfaAuthority(String urnString, String hrn, Location location, Map<ServerType, URL> urls, List<ProxyInfo> proxies, Gid gid, String type) throws JFedException {
        assert urnString != null : "Cannot create GeniAuthority if URN is not known";

        this.urnString = urnString;
        this.urn = GeniUrn.parse(urnString);
        this.urnPart = urnPartFromUrn(urnString);
        this.hrn = hrn;
        this.location = location;

        this.urls = new HashMap<>();
        if (urls != null)
            this.urls.putAll(urls);
        this.proxies = proxies == null ? new ArrayList<>() : new ArrayList<>(proxies);

        this.gid = gid;

        this.type = type;

        this.allowedCertificateHostnameAliases = new ArrayList<>();

        //these defaults are overwritten by the classes that implement another source
        source = InfoSource.USER_PROVIDED;
        this.wasStored = false;

        this.defaultScsUrn = null;
    }

    public LegacySfaAuthority(String urnString) throws JFedException {
        this.urnString = urnString;
        this.urn = GeniUrn.parse(urnString);
        this.urnPart = urnPartFromUrn(urnString);

        this.hrn = null;
        this.location = null;
        this.urls = new HashMap<>();
        this.proxies = new ArrayList<>();
        this.gid = null;

        this.allowedCertificateHostnameAliases = new ArrayList<>();

        this.defaultScsUrn = null;
    }

    /**
     * Get urnPart from urn, and check if URN is correct
     */
    public static String urnPartFromUrn(String urn) throws JFedException {
        assert urn != null;

        GeniUrn geniUrn = GeniUrn.parse(urn);
        if (geniUrn == null)
            throw new JFedException("Illegal Geni Authority URN: \"" + urn + "\"");

        String urnPart = geniUrn.getEncodedTopLevelAuthority();
        if (urnPart == null)
            throw new JFedException("Illegal Geni Authority URN: urnPart=null in \"" + urn + "\"");
        if (urnPart.length() < 2)
            throw new JFedException("Illegal Geni Authority URN: urnPart=\"" + urnPart + "\" in \"" + urn + "\"");
        if (Objects.equals(urnPart, urn))
            throw new JFedException("Error extracting urnPart from urn=\"" + urn + "\"");

        return urnPart;
    }

    public static LegacySfaAuthority fromXml(String xml) throws JFedException {
        try {
            Class docClass = be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority.class;
            String packageName = docClass.getPackage().getName();
            JAXBContext jc = JAXBContext.newInstance(packageName);
            Unmarshaller u = jc.createUnmarshaller();
            JAXBElement<be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority> doc =
                    (JAXBElement<be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority>) u.unmarshal(new StringReader(xml));
            be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority c = doc.getValue();

            return fromXml(c);
        } catch (JFedException ex) {
            throw ex;
        } catch (Exception e) {
            LOG.warn("Error reading Authority XML", e);
            e.printStackTrace();
            return null;
        }
    }

    /**
     *
     * @param xmlAuthority  Authority to be converted
     * @return the authority in the XML, or null if there is an error
     * @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 static LegacySfaAuthority fromXml(be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority xmlAuthority) throws JFedException {
        try {
            LegacySfaAuthority res = new LegacySfaAuthority(xmlAuthority.getUrn());
            res.wasStored = true;

            if (xmlAuthority.getGid() != null)
                res.setGid(new Gid(xmlAuthority.getGid()));
            res.setHrn(xmlAuthority.getHrn());
            if(xmlAuthority.getLocation() != null){
                res.setLocation(new Location(xmlAuthority.getLocation().getCountry(),
                        xmlAuthority.getLocation().getLatitude(),
                        xmlAuthority.getLocation().getLongitude()));
            }
            res.addPemSslTrustCert(xmlAuthority.getPemSslTrustCert());

            for (String specialId : xmlAuthority.getSpecial()) {
                try {
                    SpecialCase specialCase = SpecialCase.fromString(specialId.trim());
                    res.setSpecialCase(specialCase, true);
                } catch (IllegalArgumentException e) {
                    LOG.warn("Parsed authority XML contained unknown <special> option: \""+specialId+"\"");
                }
            }
            //for backward compatibility. this actually overwrites the special case if present
            if (xmlAuthority.isReconnectEachTime())
                res.setReconnectEachTime(xmlAuthority.isReconnectEachTime());

            res.setType(xmlAuthority.getType());

            if (xmlAuthority.getSource() != null) {
                InfoSource source = null;
                if (Objects.equals(xmlAuthority.getSource(), "Clearinghouse")) source = InfoSource.UTAH_CLEARINGHOUSE;
                if (Objects.equals(xmlAuthority.getSource(), "Builtin")) source = InfoSource.BUILTIN;
                if (Objects.equals(xmlAuthority.getSource(), "User")) source = InfoSource.USER_PROVIDED;
                res.setSource(source);
            }

            if (xmlAuthority.getAllowedCertificateHostnameAliases() != null)
                res.setAllowedCertificateHostnameAlias(xmlAuthority.getAllowedCertificateHostnameAliases().getAlias());

            for (Authorities.Authority.Urls.Serverurl xmlUrl : xmlAuthority.getUrls().getServerurl()) {
                ServerType.GeniServerRole role = null;
                if (Objects.equals(xmlUrl.getServertype().getRole(), "Slice Authority"))
                    role = ServerType.GeniServerRole.PROTOGENI_SA;
                if (Objects.equals(xmlUrl.getServertype().getRole(), "Aggregate Manager")) role = ServerType.GeniServerRole.AM;
                if (Objects.equals(xmlUrl.getServertype().getRole(), "PlanetLab registry"))
                    role = ServerType.GeniServerRole.PlanetLabSliceRegistry;
                if (role == null) {
                    try {
                        role = ServerType.GeniServerRole.valueOf(xmlUrl.getServertype().getRole());
                    } catch (IllegalArgumentException e) {
                        LOG.error("Unknown role: \""+xmlUrl.getServertype().getRole()+"\"", e);
                        role = null;
                    }
                }

//                assert role != null : "invalid role in XML: " + xmlUrl.getServertype().getRole();
                if (role != null) {
                    ServerType serverType = new ServerType(role, xmlUrl.getServertype().getVersion());
                    res.setUrl(serverType, new URL(xmlUrl.getUrl()));
                } else {
                    LOG.error("Ignoring URL for unknown role: \""+xmlUrl.getServertype().getRole()+"\"");
                }
            }

            List<ProxyInfo> proxies = new ArrayList<>();
            if (xmlAuthority.getProxies() != null && xmlAuthority.getProxies().getProxy() != null)
                for (Authorities.Authority.Proxies.Proxy proxyXml : xmlAuthority.getProxies().getProxy()) {
                    ProxyInfo proxyInfo = new ProxyInfo(proxyXml);
                    proxies.add(proxyInfo);
                }
            res.replaceProxies(proxies);

            if (xmlAuthority.getDefaultScsUrn() != null)
                try {
                    res.defaultScsUrn = new GeniUrn(xmlAuthority.getDefaultScsUrn());
                } catch (GeniUrn.GeniUrnParseException e) {
                    LOG.warn("defaultScsUrn is invalid URN: \""+xmlAuthority.getDefaultScsUrn()+"\"", e);
                }

            return res;
        } catch (JFedException ex) {
            throw ex;
        } catch (Exception e) {
            LOG.error("WARNING: Error reading authority xml: " + e.getMessage(), e);
            return null;
        }
    }

    public static List<GeniUrn> authoritiesToUrns(LegacySfaAuthority auth) {
        List<GeniUrn> res = new ArrayList<>();
        res.add(auth.getUrn());
        return res;
    }
    public static List<GeniUrn> authoritiesToUrns(LegacySfaAuthority... auths) {
        return Arrays.stream(auths).map(LegacySfaAuthority::getUrn).collect(Collectors.toList());
    }
    public static List<GeniUrn> authoritiesToUrns(List<LegacySfaAuthority> auths) {
        return auths.stream().map(LegacySfaAuthority::getUrn).collect(Collectors.toList());
    }

    /**
     * Synonym for {@link LegacySfaAuthority#getHrn()}
     */
    public String getName() {
        if (hrn == null) return urnPart;
        return hrn;
    }

    public void setName(String name) {
        this.hrn = name;
    }

    public String getHrn() {
        return getName();
    }

    public void setHrn(String hrn) {
        this.hrn = hrn;
    }

    public Location getLocation() {
        return location;
    }

    public void setLocation(Location location) {
        this.location = location;
    }

    public URL getUrl(ServerType serverType) {
        return urls.get(serverType);
    }

    public URL getUrl(ServerType.GeniServerRole role, int version) {
        return urls.get(new ServerType(role, version));
    }

    public Map<ServerType, URL> getUrls() {
        return Collections.unmodifiableMap(urls);
    }

    public List<ProxyInfo> getProxies() {
        return Collections.unmodifiableList(proxies);
    }

    public boolean isReconnectEachTime() {
        return hasSpecialCase(SpecialCase.RECONNECT_EACH_TIME);
    }

    public void setReconnectEachTime(boolean reconnectEachTime) {
        setSpecialCase(SpecialCase.RECONNECT_EACH_TIME, reconnectEachTime);
    }

    public String getType() {
        return type;
    }

    /**
     * isType is just a convenience to use instead of getType().equalsIgnoreCase("sometype").
     * note that is handles the type == null case
     * */
    public boolean isType(String checkType) {
        if (type == null) return false;
        if (checkType == null) {
            LOG.warn("authority.isType(null) called! This makes little sense (will handle by replying false).");
            return false;
        }
        return type.equalsIgnoreCase(checkType);
    }

    public void setType(String type) {
        this.type = type;
    }

    /**
     * Sometimes, the SSL cerificate for a host does not have the correct name for that host. For example, if the url is:
     * https://sfa.planet-lab.eu:12347, while the certificate is for "ple". To fix these issues, add allowed aliases for
     * the certificate here. In the example, you would add "ple" to this list.
     */
    public List<String> getAllowedCertificateHostnameAliases() {
        if (allowedCertificateHostnameAliases == null)
            return new LinkedList<>();
        return Collections.unmodifiableList(allowedCertificateHostnameAliases);
    }

    public void setUrl(ServerType serverType, URL newUrl) {
        urls.put(serverType, newUrl);
    }

    public void replaceUrls(Map<ServerType, URL> newMap) {
        urls.clear();
        urls.putAll(newMap);
    }

    public void replaceProxies(List<ProxyInfo> proxies) {
        this.proxies = proxies;
    }

    /**
     * Sometimes, the SSL cerificate for a host does not have the correct name for that host. For example, if the url is:
     * https://sfa.planet-lab.eu:12347, while the certificate is for "ple". To fix these issues, add allowed aliases for
     * the certificate here. In the example, you would add "ple" to this list.
     */
    public void addAllowedCertificateHostnameAlias(String alias) {
        allowedCertificateHostnameAliases.add(alias);
    }

    public void setAllowedCertificateHostnameAlias(List<String> aliases) {
        allowedCertificateHostnameAliases.clear();
        allowedCertificateHostnameAliases.addAll(aliases);
    }

    public void updateAll(String hrn, Map<ServerType, URL> urls, Gid gid, String type) {
        updateHrn(hrn);
        for (Map.Entry<ServerType, URL> e : urls.entrySet())
            updateUrl(e.getKey(), e.getValue());
        updateGid(gid);
        updateType(type);
    }

    public void updateUrl(ServerType serverType, URL newUrl) {
        URL prevUrl = getUrl(serverType);
        if (prevUrl == null) return; //no update if not set
        if (!Objects.equals(prevUrl, newUrl) && debug)
            LOG.warn("updateUrl(" + serverType + ", \"" + newUrl + "\") while prevUrl=\"" + prevUrl + "\"");
        urls.put(serverType, newUrl);
    }

    public boolean hasAmUrl() {
        for (ServerType st : urls.keySet())
            if (st.getRole() == ServerType.GeniServerRole.AM)
                return true;
        return false;
    }

    public void updateHrn(String hrn) {
        if (hrn == null) return; //no update if not set
        if (this.hrn != null && !Objects.equals(hrn, this.hrn) && debug)
            LOG.warn("updateHrn(\"" + hrn + "\") while hrn=\"" + this.hrn + "\"");
        this.hrn = hrn;
    }

    public void updateUrn(String urn) throws JFedException {
        if (urn == null) return; //no update if not set
        if (this.urnString != null && !Objects.equals(urn, this.urnString) && debug)
            LOG.warn("updateUrn(\"" + urn + "\") while urn=\"" + this.urnString + "\"");

        if (this.urnString == null || !Objects.equals(urn, this.urnString)) {
            this.urn = GeniUrn.parse(urnString);
            this.urnString = urn;
            this.urnPart = urnPartFromUrn(urn);
        }
    }

    /**
     * Synonym for {@link LegacySfaAuthority#updateHrn(String)}
     */
    public void updateName(String name) {
        updateHrn(name);
    }

    public void updateGid(Gid gid) {
        if (gid == null) return; //no update if not set
        if (this.gid != null && !Objects.equals(gid, this.gid) && debug)
            LOG.warn("updateGid(\"" + gid + "\") while gid=\"" + this.gid + "\"");
        this.gid = gid;
    }

    public void updateType(String type) {
        if (type == null) return; //no update if not set
        if (this.type != null && !Objects.equals(type, this.type) && debug)
            LOG.warn("updateType(\"" + type + "\") while type=\"" + this.type + "\"");
        this.type = type;
    }

    /**
     * the authority as named in an URN. This is useful to construct slice and user URNs.
     */
    public String getNameForUrn() {
        return urnPart;
    }

    public String getUrnString() {
        return urnString;
    }

    public GeniUrn getUrn() {
        return urn;
    }

    public Gid getGid() {
        return gid;
    }

    public void setGid(Gid gid) {
        this.gid = gid;
    }

    public void clearPemSslTrustCerts() {
        this.pemSslTrustCerts = null;
    }

    public void addPemSslTrustCert(String pemSslTrustCert) {
        if (this.pemSslTrustCerts == null) this.pemSslTrustCerts = new ArrayList<>();
        this.pemSslTrustCerts.add(pemSslTrustCert);
    }

    public void addPemSslTrustCert(List<String> extraPemSslTrustCerts) {
        if (extraPemSslTrustCerts == null) return;
        if (this.pemSslTrustCerts == null) this.pemSslTrustCerts = new ArrayList<>();
        this.pemSslTrustCerts.addAll(extraPemSslTrustCerts);
    }

    public void addPemSslTrustCert(X509Certificate sslTrustCert) {
        assert sslTrustCert != null;

        addPemSslTrustCert(KeyUtil.x509certificateToPem(sslTrustCert));
    }

    /**
     * @return a list of PEM certificates. may be empty. never null.
     */
    public List<String> getPemSslTrustCerts() {
        if (pemSslTrustCerts == null) return new ArrayList<>();
        return Collections.unmodifiableList(pemSslTrustCerts);
    }

    public boolean hasPemSslTrustCerts() {
        return pemSslTrustCerts != null && !pemSslTrustCerts.isEmpty();
    }

    public InfoSource getSource() {
        return source;
    }

    public void setSource(InfoSource source) {
        this.source = source;
    }


    public GeniUrn getDefaultScsUrn() {
        return defaultScsUrn;
    }

    public void setDefaultScsUrn(GeniUrn defaultScsUrn) {
        this.defaultScsUrn = defaultScsUrn;
    }


    /*
    * Note about the XML binding:
    *
    * XML binding generated with JAXB from authority.xsd
    * command line was:
    *   cd <main project dir>
    *   cd jfed-library/src/main/java/
    *   xjc -p be.iminds.ilabt.jfed.lowlevel.authority.binding <main project dir>/jfed-library/src/main/resources/be/iminds/ilabt/jfed/lowlevel/authority/authority.xsd
    * */

    public boolean isWasStored() {
        return wasStored;
    }

    public void setWasStored(boolean wasStored) {
        this.wasStored = wasStored;
    }

    public String toXmlString() {
        return toXmlString(true, false);
    }

    public String toXmlString(boolean omitDeclaration, boolean noNamespace) {
        try {
            StringWriter stringWriter = new StringWriter();
            //nonamespace doesn't work

            be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority xmlAuth = toXml();
            JAXBContext context = JAXBContext.newInstance(Authorities.Authority.class);
            Marshaller m = context.createMarshaller();
            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,Boolean.TRUE);
            if (omitDeclaration) {
                m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
            }

            //see http://stackoverflow.com/questions/819720/no-xmlrootelement-generated-by-jaxb
            m.marshal(new JAXBElement<>(
                            new QName("http://jfed.iminds.be/authority", "authority"),
                            be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority.class,
                            xmlAuth),
                    stringWriter);

            stringWriter.close();

            return stringWriter.toString();
        } catch (JAXBException | IOException e) {
            LOG.error("Exception marshaling SfaAuthorityBackwardCompatibility to xml", e);
        }
        return null;
    }

    public be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority toXml() {
        be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority res =
                new be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority();

        res.setUrn(urnString);
        if (gid != null) res.setGid(gid.toString());
        if (hrn != null) res.setHrn(hrn);
        if(location != null){
            be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority.Location resLocation = new Authorities.Authority.Location();
            resLocation.setCountry(location.getCountry());
            resLocation.setLatitude(location.getLatitude());
            resLocation.setLongitude(location.getLongitude());
            res.setLocation(resLocation);
        }
        if (pemSslTrustCerts != null) res.getPemSslTrustCert().addAll(pemSslTrustCerts);
        if (type != null) res.setType(type);

        for (SpecialCase sc : specialCases)
            res.getSpecial().add(sc.getId());
        //for backward compatibility, also set RECONNECT_EACH_TIME
        res.setReconnectEachTime(hasSpecialCase(SpecialCase.RECONNECT_EACH_TIME));

        if (source != null) {
            String sourceString = null;
            switch (source) {
                case UTAH_CLEARINGHOUSE: {
                    sourceString = "Clearinghouse";
                    break;
                }
                case BUILTIN: {
                    sourceString = "Builtin";
                    break;
                }
                case USER_PROVIDED: {
                    sourceString = "User";
                    break;
                }
            }
            res.setSource(sourceString);
        }

        if (allowedCertificateHostnameAliases != null && !allowedCertificateHostnameAliases.isEmpty()) {
            Authorities.Authority.AllowedCertificateHostnameAliases
                    xmlAllowedCertificateHostnameAliases =
                    new Authorities.Authority.AllowedCertificateHostnameAliases();
            for (String alias : allowedCertificateHostnameAliases)
                xmlAllowedCertificateHostnameAliases.getAlias().add(alias);
            res.setAllowedCertificateHostnameAliases(xmlAllowedCertificateHostnameAliases);
        }

        Authorities.Authority.Urls xmlUrls =
                new Authorities.Authority.Urls();
        for (Map.Entry<ServerType, URL> e : urls.entrySet()) {
            Authorities.Authority.Urls.Serverurl xmlServerUrl = new Authorities.Authority.Urls.Serverurl();
            Authorities.Authority.Urls.Serverurl.Servertype xmlServerType =
                    new Authorities.Authority.Urls.Serverurl.Servertype();
            String roleString = e.getKey().getRole().toString();
            if (Objects.equals(e.getKey().getRole(), ServerType.GeniServerRole.PROTOGENI_SA)) roleString = "Slice Authority";
            if (Objects.equals(e.getKey().getRole(), ServerType.GeniServerRole.AM)) roleString = "Aggregate Manager";
            if (Objects.equals(e.getKey().getRole(), ServerType.GeniServerRole.PlanetLabSliceRegistry))
                roleString = "PlanetLab registry";
            xmlServerType.setRole(roleString);
            xmlServerType.setVersion(e.getKey().getVersion());
            xmlServerUrl.setServertype(xmlServerType);
            xmlServerUrl.setUrl(e.getValue().toExternalForm());
            xmlUrls.getServerurl().add(xmlServerUrl);
        }
        res.setUrls(xmlUrls);

        Authorities.Authority.Proxies proxiesXml = new Authorities.Authority.Proxies();
        for (ProxyInfo proxyInfo : proxies) {
            Authorities.Authority.Proxies.Proxy proxyXml = new Authorities.Authority.Proxies.Proxy();
            proxyXml.setHostKey(proxyInfo.getHostKey());
            proxyXml.setHostname(proxyInfo.getHostname());
            proxyXml.setPortRange(proxyInfo.getPortRangeString());
            proxyXml.setProxyType(proxyInfo.getProxyType());
            proxiesXml.getProxy().add(proxyXml);
        }
        res.setProxies(proxiesXml);

        if (defaultScsUrn != null)
            res.setDefaultScsUrn(defaultScsUrn.getValue());

        return res;
        //to string:
//        StringWriter sw = new StringWriter();
//        try {
//            JAXBContext context = JAXBContext.newInstance(be.iminds.ilabt.jfed.lowlevel.authority.binding.Authorities.Authority.class);
//            Marshaller m = context.createMarshaller();
//            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
//            m.marshal(res, sw);
//            return sw.getBuffer().toString();
//        } catch (JAXBException e) {
//            //TODO: this should be handled better
//            //just dump the error to the output
//            e.printStackTrace();
//            return "Error converting to SfaAuthorityBackwardCompatibility to XML: "+ e;
//        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        LegacySfaAuthority that = (LegacySfaAuthority) o;

        if (!Objects.equals(urnString, that.urnString)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return urnString.hashCode();
    }

    @Override
    public String toString() {
        return "SfaAuthorityBackwardCompatibility{" +
                "urn='" + urnString + '\'' +
                '}';
    }

    public boolean isFake() {
        return Objects.equals(FAKE_TYPE, type);
    }

    public boolean isReal() {
        return !isFake();
    }

    public void setSpecialCase(SpecialCase specialCase, boolean enabled) {
        if (enabled)
            specialCases.add(specialCase);
        else
            specialCases.remove(specialCase);
    }

    public Collection<SpecialCase> getSpecialCases() {
        return specialCases;
    }

    public boolean hasSpecialCase(SpecialCase specialCase) {
        return specialCases.contains(specialCase);
        //todo info should come from testbeds.xml

//        switch (specialCase) {
//            case FORCE_STATUS_V2: {
//                //see FEDIBBTDEV-736
//                if (type != null && (type.equalsIgnoreCase("emulab") || type.equalsIgnoreCase("instageni")))
//                    return true;
//                else
//                    return false;
//            }
//            case FORCE_DESCRIBE_V2: {
////                if (type.equalsIgnoreCase("emulab") || type.equalsIgnoreCase("instageni"))
////                    return true;
////                else
//                return false;
//            }
//            case FORCE_V2: {
//                return false;
//            }
//            case FORCE_V3: {
//                return false;
//            }
//            default: throw new RuntimeException("jFed bug: Special case unimplemented: "+specialCase);
//        }
    }

    /**
     * returns true if this authority is authorative for the given URN.
     * This is not just a matter of comparing the URN, it's more complex due to sub authorities:
     *
     * (Most authorities are authorative over their sub authorities, but some special cases are not. (to be confoirmed this case exists))
     *
     * For some authorities, there is no top level, there are only sub authorities.
     * */
    public boolean isAuthoritative(GeniUrn anyUrn) {
        String thisUrnPart = getNameForUrn();
        String checkedUrnPart = anyUrn.getEncodedTopLevelAuthority();
        return checkedUrnPart.startsWith(thisUrnPart);
    }

    public enum SpecialCase {
        HIDE_IN_GUI("hide_in_gui"),
        RECONNECT_EACH_TIME("must_reconnect_each_call"),
        FORCE_DESCRIBE_V2("force_amv2_describe"),
        FORCE_STATUS_V2("force_amv2_status"),
        FORCE_DELETE_V2("force_amv2_delete"),
        FORCE_V2("force_amv2_all"),
        FORCE_V3("force_amv3_all"),
        REMOVE_ABAC_CRED("remove_abac_credential"),
        REMOVE_SFA_CRED("remove_sfa_credential"),
        PREFER_TOPLEVEL("prefer_toplevel_over_subauthority"),
        CENTRAL_BROKER_AUTH("central_broker_architecture_brokerauth"),
        CENTRAL_BROKER_SUBAUTH("central_broker_architecture_subauth"),
        /**
         * if true, the advertisement RSpec contains an up to date list with available VLANs, instead of a static list.
         *
         * This can be used to make any smarter.
         *
         * see FEDIBBTDEV-1251
         */
        STITCHING_ADVERTISEMENT_RSPEC_UPTODATE("stitching_advertisement_rspec_uptodate"),
        /**
         * see FEDIBBTDEV-1039
         */
        STITCHING_SUPPORTS_ANY("stitching_supports_any"),
        /**
         * Used for ION which sets default expire to 24 hours (regardless of slice expiration time),
         * but allows Renew to increase or decrease it.
         *
         * see FEDIBBTDEV-903
         */
        FORCE_RENEW_AFTER_CREATESLIVER("force_renew_after_createsliver"),
        IGNORE_PROVISION_OPERATIONAL_STATUS("ignore_provision_operational_status"),
        /**
         * see FEDIBBTDEV-1085
         */
        SUPPORTS_UPDATE_SSH_KEYS("supports_poa_update_ssh_keys"),
        /**
         * see FEDIBBTDEV-1085
         */
        SUPPORTS_POA_SHARE_LAN("supports_poa_share_lan"),
        /**
         * see FEDIBBTDEV-1085
         */
        SUPPORTS_POA_NODE_RESTART("supports_poa_node_restart"),
        /**
         * see FEDIBBTDEV-1085
         */
        SUPPORTS_POA_NODE_RELOAD_OS("supports_poa_reload_os"),
        /**
         * see FEDIBBTDEV-1085
         */
        SUPPORTS_POA_OPEN_CONSOLE("supports_poa_open_console"),
        /**
         * see FEDIBBTDEV-1085
         */
        SUPPORTS_EXT_USER_DISK_IMAGE_OPS("supports_poa_ext_diskimage_ops"),
        SUPPORTS_RESERVATIONS_NITOS("supports_reservations_nitos"),
        SUPPORTS_RESERVATIONS_GENI("supports_reservations_geni"),
        /**
         * see FEDIBBTDEV-1448
         */
        ADD_EXPIRES_ATTRIBUTE_TO_REQUEST("add_expires_attribute_to_request_rspec"),
        /**
         *  GENI SA needs this when looking up project. Other SA's might not support it.
         */
        HAS_MATCH_PROJECT_EXPIRED("match_project_expired"),
        HAS_FEATURE_LOOKUP_SLICE_FOR_MEMBER_MATCH_EXPIRED("feature_lookup_slice_for_member_match_expired"),
        HAS_FEATURE_REQUEST_MEMBER_SSH_KEYS("feature_request_member_ssh_keys"),
        HAS_FEATURE_REQUEST_PROJECT_MEMBERS("feature_request_project_members"),
        /**
         * see https://github.com/GENI-NSF/geni-ch/issues/394
         */
        BUG_WORKAROUND_LOOKUP_SLICE_EXPIRED_MATCH_BOOLEAN("bug_workaround_lookup_slice_expired_match_boolean"),
        /**
         * Whether the authority should be included in the connectivity-tests.
         *
         * see FEDIBBTDEV-1675
         */
        EXCLUDE_IN_CONNECTIVITY_TEST("exclude_in_connectivity_test"),
        /**
         * SUPPORTS_STITCHING is a flag to be used by clients to guide their users.
         * It is not enforced in any way by the library or high-level library.
         *
         * see FEDIBBTDEV-1753
         */
        SUPPORTS_STITCHING("supports_stitching"),

        /**
         * fedmon has detailed info about availability of each testbed node.
         * The jFed GUI can thus skip fetching the available advertisement RSpec, and can use thiat info instead.
         * The info is better than the available info, because each week each node is tested.
         * The testbeds for which this is actived typically have many nodes that do not work.
        * */
        FEDMON_HAS_NODE_AVAILABILITY_DETAILS("fedmon_has_node_availability_details"),
        ;

        private final String id;
        SpecialCase(String id) {
            this.id = id;
        }

        public static SpecialCase fromString(String id) {
            if (id == null)
                throw new IllegalArgumentException("null is not a valid argument");
            if (id.trim().isEmpty())
                throw new IllegalArgumentException("\""+id+"\" is not a valid argument");
            for (SpecialCase sc : SpecialCase.values()) {
                if (sc.id.equalsIgnoreCase(id))
                    return sc;
            }
            throw new IllegalArgumentException("\""+id+"\" is not a known <special>");
        }

        public String getId() {
            return id;
        }
    }


    public enum InfoSource {UTAH_CLEARINGHOUSE, BUILTIN, USER_PROVIDED}

    public static class ProxyInfo {
        protected final String proxyType;
        protected final String hostname;
        protected final String portRange;
        protected final String hostKey;

        public ProxyInfo(final Authorities.Authority.Proxies.Proxy p) {
            proxyType = p.getProxyType();
            hostname = p.getHostname();
            portRange = p.getPortRange();
            hostKey = p.getHostKey();
        }

        public ProxyInfo(final ProxyInfo p) {
            proxyType = p.getProxyType();
            hostname = p.getHostname();
            portRange = p.getPortRangeString();
            hostKey = p.getHostKey();
        }

        public String getProxyType() {
            return proxyType;
        }

        public String getHostname() {
            return hostname;
        }

        public String getPortRangeString() {
            return portRange;
        }

        public VlanRange getPortRange() {
            return new VlanRange(portRange);
        }

        public String getHostKey() {
            return hostKey;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            ProxyInfo proxyInfo = (ProxyInfo) o;

            if (hostKey != null ? !Objects.equals(hostKey, proxyInfo.hostKey) : proxyInfo.hostKey != null) return false;
            if (hostname != null ? !Objects.equals(hostname, proxyInfo.hostname) : proxyInfo.hostname != null) return false;
            if (portRange != null ? !Objects.equals(portRange, proxyInfo.portRange) : proxyInfo.portRange != null) return false;
            if (proxyType != null ? !Objects.equals(proxyType, proxyInfo.proxyType) : proxyInfo.proxyType != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = proxyType != null ? proxyType.hashCode() : 0;
            result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
            result = 31 * result + (portRange != null ? portRange.hashCode() : 0);
            result = 31 * result + (hostKey != null ? hostKey.hashCode() : 0);
            return result;
        }
    }

    public static final class Location {
        private final String country;
        private final double latitude;
        private final double longitude;

        public Location(String country, double latitude, double longitude) {
            this.country = country;
            this.latitude = latitude;
            this.longitude = longitude;
        }

        public String getCountry() {
            return country;
        }

        public double getLatitude() {
            return latitude;
        }

        public double getLongitude() {
            return longitude;
        }

        @Override
        public String toString() {
            return "Location{" +
                    "country='" + country + '\'' +
                    ", latitude=" + latitude +
                    ", longitude=" + longitude +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Location location = (Location) o;

            if (Double.compare(location.latitude, latitude) != 0) return false;
            if (Double.compare(location.longitude, longitude) != 0) return false;
            if (country != null ? !Objects.equals(country, location.country) : location.country != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result;
            long temp;
            result = country != null ? country.hashCode() : 0;
            temp = Double.doubleToLongBits(latitude);
            result = 31 * result + (int) (temp ^ (temp >>> 32));
            temp = Double.doubleToLongBits(longitude);
            result = 31 * result + (int) (temp ^ (temp >>> 32));
            return result;
        }
    }
}
