package com.android.hotspot2.omadm;

import android.util.Base64;
import android.util.Log;

import com.android.anqp.eap.EAP;
import com.android.anqp.eap.EAPMethod;
import com.android.anqp.eap.ExpandedEAPMethod;
import com.android.anqp.eap.InnerAuthEAP;
import com.android.anqp.eap.NonEAPInnerAuth;
import com.android.hotspot2.IMSIParameter;
import com.android.hotspot2.Utils;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.osu.commands.MOData;
import com.android.hotspot2.pps.Credential;
import com.android.hotspot2.pps.HomeSP;
import com.android.hotspot2.pps.Policy;
import com.android.hotspot2.pps.SubscriptionParameters;
import com.android.hotspot2.pps.UpdateInfo;

import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

/**
 * Handles provisioning of PerProviderSubscription data.
 */
public class MOManager {

    public static final String TAG_AAAServerTrustRoot = "AAAServerTrustRoot";
    public static final String TAG_AbleToShare = "AbleToShare";
    public static final String TAG_CertificateType = "CertificateType";
    public static final String TAG_CertSHA256Fingerprint = "CertSHA256Fingerprint";
    public static final String TAG_CertURL = "CertURL";
    public static final String TAG_CheckAAAServerCertStatus = "CheckAAAServerCertStatus";
    public static final String TAG_Country = "Country";
    public static final String TAG_CreationDate = "CreationDate";
    public static final String TAG_Credential = "Credential";
    public static final String TAG_CredentialPriority = "CredentialPriority";
    public static final String TAG_DataLimit = "DataLimit";
    public static final String TAG_DigitalCertificate = "DigitalCertificate";
    public static final String TAG_DLBandwidth = "DLBandwidth";
    public static final String TAG_EAPMethod = "EAPMethod";
    public static final String TAG_EAPType = "EAPType";
    public static final String TAG_ExpirationDate = "ExpirationDate";
    public static final String TAG_Extension = "Extension";
    public static final String TAG_FQDN = "FQDN";
    public static final String TAG_FQDN_Match = "FQDN_Match";
    public static final String TAG_FriendlyName = "FriendlyName";
    public static final String TAG_HESSID = "HESSID";
    public static final String TAG_HomeOI = "HomeOI";
    public static final String TAG_HomeOIList = "HomeOIList";
    public static final String TAG_HomeOIRequired = "HomeOIRequired";
    public static final String TAG_HomeSP = "HomeSP";
    public static final String TAG_IconURL = "IconURL";
    public static final String TAG_IMSI = "IMSI";
    public static final String TAG_InnerEAPType = "InnerEAPType";
    public static final String TAG_InnerMethod = "InnerMethod";
    public static final String TAG_InnerVendorID = "InnerVendorID";
    public static final String TAG_InnerVendorType = "InnerVendorType";
    public static final String TAG_IPProtocol = "IPProtocol";
    public static final String TAG_MachineManaged = "MachineManaged";
    public static final String TAG_MaximumBSSLoadValue = "MaximumBSSLoadValue";
    public static final String TAG_MinBackhaulThreshold = "MinBackhaulThreshold";
    public static final String TAG_NetworkID = "NetworkID";
    public static final String TAG_NetworkType = "NetworkType";
    public static final String TAG_Other = "Other";
    public static final String TAG_OtherHomePartners = "OtherHomePartners";
    public static final String TAG_Password = "Password";
    public static final String TAG_PerProviderSubscription = "PerProviderSubscription";
    public static final String TAG_Policy = "Policy";
    public static final String TAG_PolicyUpdate = "PolicyUpdate";
    public static final String TAG_PortNumber = "PortNumber";
    public static final String TAG_PreferredRoamingPartnerList = "PreferredRoamingPartnerList";
    public static final String TAG_Priority = "Priority";
    public static final String TAG_Realm = "Realm";
    public static final String TAG_RequiredProtoPortTuple = "RequiredProtoPortTuple";
    public static final String TAG_Restriction = "Restriction";
    public static final String TAG_RoamingConsortiumOI = "RoamingConsortiumOI";
    public static final String TAG_SIM = "SIM";
    public static final String TAG_SoftTokenApp = "SoftTokenApp";
    public static final String TAG_SPExclusionList = "SPExclusionList";
    public static final String TAG_SSID = "SSID";
    public static final String TAG_StartDate = "StartDate";
    public static final String TAG_SubscriptionParameters = "SubscriptionParameters";
    public static final String TAG_SubscriptionUpdate = "SubscriptionUpdate";
    public static final String TAG_TimeLimit = "TimeLimit";
    public static final String TAG_TrustRoot = "TrustRoot";
    public static final String TAG_TypeOfSubscription = "TypeOfSubscription";
    public static final String TAG_ULBandwidth = "ULBandwidth";
    public static final String TAG_UpdateIdentifier = "UpdateIdentifier";
    public static final String TAG_UpdateInterval = "UpdateInterval";
    public static final String TAG_UpdateMethod = "UpdateMethod";
    public static final String TAG_URI = "URI";
    public static final String TAG_UsageLimits = "UsageLimits";
    public static final String TAG_UsageTimePeriod = "UsageTimePeriod";
    public static final String TAG_Username = "Username";
    public static final String TAG_UsernamePassword = "UsernamePassword";
    public static final String TAG_VendorId = "VendorId";
    public static final String TAG_VendorType = "VendorType";

    public static final long IntervalFactor = 60000L;  // All MO intervals are in minutes

    private static final DateFormat DTFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    private static final Map<String, Map<String, Object>> sSelectionMap;

    static {
        DTFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

        sSelectionMap = new HashMap<>();

        setSelections(TAG_FQDN_Match,
                "exactmatch", Boolean.FALSE,
                "includesubdomains", Boolean.TRUE);
        setSelections(TAG_UpdateMethod,
                "oma-dm-clientinitiated", Boolean.FALSE,
                "spp-clientinitiated", Boolean.TRUE);
        setSelections(TAG_Restriction,
                "homesp", UpdateInfo.UpdateRestriction.HomeSP,
                "roamingpartner", UpdateInfo.UpdateRestriction.RoamingPartner,
                "unrestricted", UpdateInfo.UpdateRestriction.Unrestricted);
    }

    private static void setSelections(String key, Object... pairs) {
        Map<String, Object> kvp = new HashMap<>();
        sSelectionMap.put(key, kvp);
        for (int n = 0; n < pairs.length; n += 2) {
            kvp.put(pairs[n].toString(), pairs[n + 1]);
        }
    }

    private final File mPpsFile;
    private final boolean mEnabled;
    private final Map<String, HomeSP> mSPs;

    public MOManager(File ppsFile, boolean hs2enabled) {
        mPpsFile = ppsFile;
        mEnabled = hs2enabled;
        mSPs = new HashMap<>();
    }

    public File getPpsFile() {
        return mPpsFile;
    }

    public boolean isEnabled() {
        return mEnabled;
    }

    public boolean isConfigured() {
        return mEnabled && !mSPs.isEmpty();
    }

    public Map<String, HomeSP> getLoadedSPs() {
        return Collections.unmodifiableMap(mSPs);
    }

    public List<HomeSP> loadAllSPs() throws IOException {

        if (!mEnabled || !mPpsFile.exists()) {
            return Collections.emptyList();
        }

        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
            MOTree moTree = MOTree.unmarshal(in);
            mSPs.clear();
            if (moTree == null) {
                return Collections.emptyList();     // Empty file
            }

            List<HomeSP> sps = buildSPs(moTree);
            if (sps != null) {
                for (HomeSP sp : sps) {
                    if (mSPs.put(sp.getFQDN(), sp) != null) {
                        throw new OMAException("Multiple SPs for FQDN '" + sp.getFQDN() + "'");
                    } else {
                        Log.d(OSUManager.TAG, "retrieved " + sp.getFQDN() + " from PPS");
                    }
                }
                return sps;

            } else {
                throw new OMAException("Failed to build HomeSP");
            }
        }
    }

    public static HomeSP buildSP(String xml) throws IOException, SAXException {
        OMAParser omaParser = new OMAParser();
        MOTree tree = omaParser.parse(xml, OMAConstants.PPS_URN);
        List<HomeSP> spList = buildSPs(tree);
        if (spList.size() != 1) {
            throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
        }
        return spList.iterator().next();
    }

    public HomeSP addSP(String xml, OSUManager osuManager) throws IOException, SAXException {
        OMAParser omaParser = new OMAParser();
        return addSP(omaParser.parse(xml, OMAConstants.PPS_URN), osuManager);
    }

    private static final List<String> FQDNPath = Arrays.asList(TAG_HomeSP, TAG_FQDN);

    /**
     * R1 *only* addSP method.
     *
     * @param homeSP
     * @throws IOException
     */
    public void addSP(HomeSP homeSP, OSUManager osuManager) throws IOException {
        if (!mEnabled) {
            throw new IOException("HS2.0 not enabled on this device");
        }
        if (mSPs.containsKey(homeSP.getFQDN())) {
            Log.d(OSUManager.TAG, "HS20 profile for " +
                    homeSP.getFQDN() + " already exists");
            return;
        }
        Log.d(OSUManager.TAG, "Adding new HS20 profile for " + homeSP.getFQDN());

        OMAConstructed dummyRoot = new OMAConstructed(null, TAG_PerProviderSubscription, null);
        buildHomeSPTree(homeSP, dummyRoot, mSPs.size() + 1);
        try {
            addSP(dummyRoot, osuManager);
        } catch (FileNotFoundException fnfe) {
            MOTree tree =
                    MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
            // No file to load a pre-build MO tree from, create a new one and save it.
            //MOTree tree = new MOTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
            writeMO(tree, mPpsFile, osuManager);
        }
        mSPs.put(homeSP.getFQDN(), homeSP);
    }

    public HomeSP addSP(MOTree instanceTree, OSUManager osuManager) throws IOException {
        List<HomeSP> spList = buildSPs(instanceTree);
        if (spList.size() != 1) {
            throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
        }

        HomeSP sp = spList.iterator().next();
        String fqdn = sp.getFQDN();
        if (mSPs.put(fqdn, sp) != null) {
            throw new OMAException("SP " + fqdn + " already exists");
        }

        OMAConstructed pps = (OMAConstructed) instanceTree.getRoot().
                getChild(TAG_PerProviderSubscription);

        try {
            addSP(pps, osuManager);
        } catch (FileNotFoundException fnfe) {
            MOTree tree = new MOTree(instanceTree.getUrn(), instanceTree.getDtdRev(),
                    instanceTree.getRoot());
            writeMO(tree, mPpsFile, osuManager);
        }

        return sp;
    }

    /**
     * Add an SP sub-tree. mo must be PPS with an immediate instance child (e.g. Cred01) and an
     * optional UpdateIdentifier,
     *
     * @param mo The new MO
     * @throws IOException
     */
    private void addSP(OMANode mo, OSUManager osuManager) throws IOException {
        MOTree moTree;
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
            moTree = MOTree.unmarshal(in);
            moTree.getRoot().addChild(mo);

                /*
            OMAConstructed ppsRoot = (OMAConstructed)
                    moTree.getRoot().addChild(TAG_PerProviderSubscription, "", null, null);
            for (OMANode child : mo.getChildren()) {
                ppsRoot.addChild(child);
                if (!child.isLeaf()) {
                    moTree.getRoot().addChild(child);
                }
                else if (child.getName().equals(TAG_UpdateIdentifier)) {
                    OMANode currentUD = moTree.getRoot().getChild(TAG_UpdateIdentifier);
                    if (currentUD != null) {
                        moTree.getRoot().replaceNode(currentUD, child);
                    }
                    else {
                        moTree.getRoot().addChild(child);
                    }
                }
            }
                */
        }
        writeMO(moTree, mPpsFile, osuManager);
    }

    private static OMAConstructed findTargetTree(MOTree moTree, String fqdn) throws OMAException {
        OMANode pps = moTree.getRoot();
        for (OMANode node : pps.getChildren()) {
            OMANode instance = null;
            if (node.getName().equals(TAG_PerProviderSubscription)) {
                instance = getInstanceNode((OMAConstructed) node);
            } else if (!node.isLeaf()) {
                instance = node;
            }
            if (instance != null) {
                String nodeFqdn = getString(instance.getListValue(FQDNPath.iterator()));
                if (fqdn.equalsIgnoreCase(nodeFqdn)) {
                    return (OMAConstructed) node;
                    // targetTree is rooted at the PPS
                }
            }
        }
        return null;
    }

    private static OMAConstructed getInstanceNode(OMAConstructed root) throws OMAException {
        for (OMANode child : root.getChildren()) {
            if (!child.isLeaf()) {
                return (OMAConstructed) child;
            }
        }
        throw new OMAException("Cannot find instance node");
    }

    public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods, OSUManager osuManager)
            throws IOException {

        Log.d(OSUManager.TAG, "modifying SP: " + mods);
        MOTree moTree;
        int ppsMods = 0;
        int updateIdentifier = 0;
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
            moTree = MOTree.unmarshal(in);
            // moTree is PPS/?/provider-data

            OMAConstructed targetTree = findTargetTree(moTree, homeSP.getFQDN());
            if (targetTree == null) {
                throw new IOException("Failed to find PPS tree for " + homeSP.getFQDN());
            }
            OMAConstructed instance = getInstanceNode(targetTree);

            for (MOData mod : mods) {
                LinkedList<String> tailPath =
                        getTailPath(mod.getBaseURI(), TAG_PerProviderSubscription);
                OMAConstructed modRoot = mod.getMOTree().getRoot();
                // modRoot is the MgmtTree with the actual object as a direct child
                // (e.g. Credential)

                if (tailPath.getFirst().equals(TAG_UpdateIdentifier)) {
                    updateIdentifier = getInteger(modRoot.getChildren().iterator().next());
                    OMANode oldUdi = targetTree.getChild(TAG_UpdateIdentifier);
                    if (getInteger(oldUdi) != updateIdentifier) {
                        ppsMods++;
                    }
                    if (oldUdi != null) {
                        targetTree.replaceNode(oldUdi, modRoot.getChild(TAG_UpdateIdentifier));
                    } else {
                        targetTree.addChild(modRoot.getChild(TAG_UpdateIdentifier));
                    }
                } else {
                    tailPath.removeFirst();     // Drop the instance
                    OMANode current = instance.getListValue(tailPath.iterator());
                    if (current == null) {
                        throw new IOException("No previous node for " + tailPath + " in " +
                                homeSP.getFQDN());
                    }
                    for (OMANode newNode : modRoot.getChildren()) {
                        // newNode is something like Credential
                        // current is the same existing node
                        OMANode old = current.getParent().replaceNode(current, newNode);
                        ppsMods++;
                    }
                }
            }
        }
        writeMO(moTree, mPpsFile, osuManager);

        if (ppsMods == 0) {
            return null;    // HomeSP not modified.
        }

        // Return a new rebuilt HomeSP
        List<HomeSP> sps = buildSPs(moTree);
        if (sps != null) {
            for (HomeSP sp : sps) {
                if (sp.getFQDN().equals(homeSP.getFQDN())) {
                    return sp;
                }
            }
        } else {
            throw new OMAException("Failed to build HomeSP");
        }
        return null;
    }

    private static LinkedList<String> getTailPath(String pathString, String rootName)
            throws IOException {
        String[] path = pathString.split("/");
        int pathIndex;
        for (pathIndex = 0; pathIndex < path.length; pathIndex++) {
            if (path[pathIndex].equalsIgnoreCase(rootName)) {
                pathIndex++;
                break;
            }
        }
        if (pathIndex >= path.length) {
            throw new IOException("Bad node-path: " + pathString);
        }
        LinkedList<String> tailPath = new LinkedList<>();
        while (pathIndex < path.length) {
            tailPath.add(path[pathIndex]);
            pathIndex++;
        }
        return tailPath;
    }

    public HomeSP getHomeSP(String fqdn) {
        return mSPs.get(fqdn);
    }

    public void removeSP(String fqdn, OSUManager osuManager) throws IOException {
        if (mSPs.remove(fqdn) == null) {
            Log.d(OSUManager.TAG, "No HS20 profile to delete for " + fqdn);
            return;
        }

        Log.d(OSUManager.TAG, "Deleting HS20 profile for " + fqdn);

        MOTree moTree;
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
            moTree = MOTree.unmarshal(in);
            OMAConstructed tbd = findTargetTree(moTree, fqdn);
            if (tbd == null) {
                throw new IOException("Node " + fqdn + " doesn't exist in MO tree");
            }
            OMAConstructed pps = moTree.getRoot();
            OMANode removed = pps.removeNode("?", tbd);
            if (removed == null) {
                throw new IOException("Failed to remove " + fqdn + " out of MO tree");
            }
        }
        writeMO(moTree, mPpsFile, osuManager);
        osuManager.spDeleted(fqdn);
    }

    public MOTree getMOTree(HomeSP homeSP) throws IOException {
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
            MOTree moTree = MOTree.unmarshal(in);
            OMAConstructed target = findTargetTree(moTree, homeSP.getFQDN());
            if (target == null) {
                throw new IOException("Can't find " + homeSP.getFQDN() + " in MO tree");
            }
            return MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, target);
        }
    }

    private static void writeMO(MOTree moTree, File f, OSUManager osuManager) throws IOException {
        try (BufferedOutputStream out =
                     new BufferedOutputStream(new FileOutputStream(f, false))) {
            moTree.marshal(out);
            out.flush();
        }
    }

    private static String fqdnList(Collection<HomeSP> sps) {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (HomeSP sp : sps) {
            if (first) {
                first = false;
            } else {
                sb.append(", ");
            }
            sb.append(sp.getFQDN());
        }
        return sb.toString();
    }

    private static OMANode buildHomeSPTree(HomeSP homeSP, OMAConstructed root, int instanceID)
            throws IOException {
        OMANode providerSubNode = root.addChild(getInstanceString(instanceID),
                null, null, null);

        // The HomeSP:
        OMANode homeSpNode = providerSubNode.addChild(TAG_HomeSP, null, null, null);
        if (!homeSP.getSSIDs().isEmpty()) {
            OMAConstructed nwkIDNode =
                    (OMAConstructed) homeSpNode.addChild(TAG_NetworkID, null, null, null);
            int instance = 0;
            for (Map.Entry<String, Long> entry : homeSP.getSSIDs().entrySet()) {
                OMAConstructed inode =
                        (OMAConstructed) nwkIDNode
                                .addChild(getInstanceString(instance++), null, null, null);
                inode.addChild(TAG_SSID, null, entry.getKey(), null);
                if (entry.getValue() != null) {
                    inode.addChild(TAG_HESSID, null,
                            String.format("%012x", entry.getValue()), null);
                }
            }
        }

        homeSpNode.addChild(TAG_FriendlyName, null, homeSP.getFriendlyName(), null);

        if (homeSP.getIconURL() != null) {
            homeSpNode.addChild(TAG_IconURL, null, homeSP.getIconURL(), null);
        }

        homeSpNode.addChild(TAG_FQDN, null, homeSP.getFQDN(), null);

        if (!homeSP.getMatchAllOIs().isEmpty() || !homeSP.getMatchAnyOIs().isEmpty()) {
            OMAConstructed homeOIList =
                    (OMAConstructed) homeSpNode.addChild(TAG_HomeOIList, null, null, null);

            int instance = 0;
            for (Long oi : homeSP.getMatchAllOIs()) {
                OMAConstructed inode =
                        (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
                                null, null, null);
                inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
                inode.addChild(TAG_HomeOIRequired, null, "TRUE", null);
            }
            for (Long oi : homeSP.getMatchAnyOIs()) {
                OMAConstructed inode =
                        (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
                                null, null, null);
                inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
                inode.addChild(TAG_HomeOIRequired, null, "FALSE", null);
            }
        }

        if (!homeSP.getOtherHomePartners().isEmpty()) {
            OMAConstructed otherPartners =
                    (OMAConstructed) homeSpNode.addChild(TAG_OtherHomePartners, null, null, null);
            int instance = 0;
            for (String fqdn : homeSP.getOtherHomePartners()) {
                OMAConstructed inode =
                        (OMAConstructed) otherPartners.addChild(getInstanceString(instance++),
                                null, null, null);
                inode.addChild(TAG_FQDN, null, fqdn, null);
            }
        }

        if (!homeSP.getRoamingConsortiums().isEmpty()) {
            homeSpNode.addChild(TAG_RoamingConsortiumOI, null,
                    getRCList(homeSP.getRoamingConsortiums()), null);
        }

        // The Credential:
        OMANode credentialNode = providerSubNode.addChild(TAG_Credential, null, null, null);
        Credential cred = homeSP.getCredential();
        EAPMethod method = cred.getEAPMethod();

        if (cred.getCtime() > 0) {
            credentialNode.addChild(TAG_CreationDate,
                    null, DTFormat.format(new Date(cred.getCtime())), null);
        }
        if (cred.getExpTime() > 0) {
            credentialNode.addChild(TAG_ExpirationDate,
                    null, DTFormat.format(new Date(cred.getExpTime())), null);
        }

        if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_SIM
                || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKA
                || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKAPrim) {

            OMANode simNode = credentialNode.addChild(TAG_SIM, null, null, null);
            simNode.addChild(TAG_IMSI, null, cred.getImsi().toString(), null);
            simNode.addChild(TAG_EAPType, null,
                    Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);

        } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TTLS) {

            OMANode unpNode = credentialNode.addChild(TAG_UsernamePassword, null, null, null);
            unpNode.addChild(TAG_Username, null, cred.getUserName(), null);
            unpNode.addChild(TAG_Password, null,
                    Base64.encodeToString(cred.getPassword().getBytes(StandardCharsets.UTF_8),
                            Base64.DEFAULT), null);
            OMANode eapNode = unpNode.addChild(TAG_EAPMethod, null, null, null);
            eapNode.addChild(TAG_EAPType, null,
                    Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
            eapNode.addChild(TAG_InnerMethod, null,
                    ((NonEAPInnerAuth) method.getAuthParam()).getOMAtype(), null);

        } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TLS) {

            OMANode certNode = credentialNode.addChild(TAG_DigitalCertificate, null, null, null);
            certNode.addChild(TAG_CertificateType, null, Credential.CertTypeX509, null);
            certNode.addChild(TAG_CertSHA256Fingerprint, null,
                    Utils.toHex(cred.getFingerPrint()), null);

        } else {
            throw new OMAException("Invalid credential on " + homeSP.getFQDN());
        }

        credentialNode.addChild(TAG_Realm, null, cred.getRealm(), null);

        // !!! Note: This node defines CRL checking through OSCP, I suspect we won't be able
        // to do that so it is commented out:
        //credentialNode.addChild(TAG_CheckAAAServerCertStatus, null, "TRUE", null);
        return providerSubNode;
    }

    private static String getInstanceString(int instance) {
        return "r1i" + instance;
    }

    private static String getRCList(Collection<Long> rcs) {
        StringBuilder builder = new StringBuilder();
        boolean first = true;
        for (Long roamingConsortium : rcs) {
            if (first) {
                first = false;
            } else {
                builder.append(',');
            }
            builder.append(String.format("%x", roamingConsortium));
        }
        return builder.toString();
    }

    public static List<HomeSP> buildSPs(MOTree moTree) throws OMAException {
        OMAConstructed spList;
        List<HomeSP> homeSPs = new ArrayList<>();
        if (moTree.getRoot().getName().equals(TAG_PerProviderSubscription)) {
            // The old PPS file was rooted at PPS instead of MgmtTree to conserve space
            spList = moTree.getRoot();

            if (spList == null) {
                return homeSPs;
            }

            for (OMANode node : spList.getChildren()) {
                if (!node.isLeaf()) {
                    homeSPs.add(buildHomeSP(node, 0));
                }
            }
        } else {
            for (OMANode ppsRoot : moTree.getRoot().getChildren()) {
                if (ppsRoot.getName().equals(TAG_PerProviderSubscription)) {
                    Integer updateIdentifier = null;
                    OMANode instance = null;
                    for (OMANode child : ppsRoot.getChildren()) {
                        if (child.getName().equals(TAG_UpdateIdentifier)) {
                            updateIdentifier = getInteger(child);
                        } else if (!child.isLeaf()) {
                            instance = child;
                        }
                    }
                    if (instance == null) {
                        throw new OMAException("PPS node missing instance node");
                    }
                    homeSPs.add(buildHomeSP(instance,
                            updateIdentifier != null ? updateIdentifier : 0));
                }
            }
        }

        return homeSPs;
    }

    private static HomeSP buildHomeSP(OMANode ppsRoot, int updateIdentifier) throws OMAException {
        OMANode spRoot = ppsRoot.getChild(TAG_HomeSP);

        String fqdn = spRoot.getScalarValue(Arrays.asList(TAG_FQDN).iterator());
        String friendlyName = spRoot.getScalarValue(Arrays.asList(TAG_FriendlyName).iterator());
        String iconURL = spRoot.getScalarValue(Arrays.asList(TAG_IconURL).iterator());

        HashSet<Long> roamingConsortiums = new HashSet<>();
        String oiString = spRoot.getScalarValue(Arrays.asList(TAG_RoamingConsortiumOI).iterator());
        if (oiString != null) {
            for (String oi : oiString.split(",")) {
                roamingConsortiums.add(Long.parseLong(oi.trim(), 16));
            }
        }

        Map<String, Long> ssids = new HashMap<>();

        OMANode ssidListNode = spRoot.getListValue(Arrays.asList(TAG_NetworkID).iterator());
        if (ssidListNode != null) {
            for (OMANode ssidRoot : ssidListNode.getChildren()) {
                OMANode hessidNode = ssidRoot.getChild(TAG_HESSID);
                ssids.put(ssidRoot.getChild(TAG_SSID).getValue(), getMac(hessidNode));
            }
        }

        Set<Long> matchAnyOIs = new HashSet<>();
        List<Long> matchAllOIs = new ArrayList<>();
        OMANode homeOIListNode = spRoot.getListValue(Arrays.asList(TAG_HomeOIList).iterator());
        if (homeOIListNode != null) {
            for (OMANode homeOIRoot : homeOIListNode.getChildren()) {
                String homeOI = homeOIRoot.getChild(TAG_HomeOI).getValue();
                if (Boolean.parseBoolean(homeOIRoot.getChild(TAG_HomeOIRequired).getValue())) {
                    matchAllOIs.add(Long.parseLong(homeOI, 16));
                } else {
                    matchAnyOIs.add(Long.parseLong(homeOI, 16));
                }
            }
        }

        Set<String> otherHomePartners = new HashSet<>();
        OMANode otherListNode =
                spRoot.getListValue(Arrays.asList(TAG_OtherHomePartners).iterator());
        if (otherListNode != null) {
            for (OMANode fqdnNode : otherListNode.getChildren()) {
                otherHomePartners.add(fqdnNode.getChild(TAG_FQDN).getValue());
            }
        }

        Credential credential = buildCredential(ppsRoot.getChild(TAG_Credential));

        OMANode policyNode = ppsRoot.getChild(TAG_Policy);
        Policy policy = policyNode != null ? new Policy(policyNode) : null;

        Map<String, String> aaaTrustRoots;
        OMANode aaaRootNode = ppsRoot.getChild(TAG_AAAServerTrustRoot);
        if (aaaRootNode == null) {
            aaaTrustRoots = null;
        } else {
            aaaTrustRoots = new HashMap<>(aaaRootNode.getChildren().size());
            for (OMANode child : aaaRootNode.getChildren()) {
                aaaTrustRoots.put(getString(child, TAG_CertURL),
                        getString(child, TAG_CertSHA256Fingerprint));
            }
        }

        OMANode updateNode = ppsRoot.getChild(TAG_SubscriptionUpdate);
        UpdateInfo subscriptionUpdate = updateNode != null ? new UpdateInfo(updateNode) : null;
        OMANode subNode = ppsRoot.getChild(TAG_SubscriptionParameters);
        SubscriptionParameters subscriptionParameters = subNode != null ?
                new SubscriptionParameters(subNode) : null;

        return new HomeSP(ssids, fqdn, roamingConsortiums, otherHomePartners,
                matchAnyOIs, matchAllOIs, friendlyName, iconURL, credential,
                policy, getInteger(ppsRoot.getChild(TAG_CredentialPriority), 0),
                aaaTrustRoots, subscriptionUpdate, subscriptionParameters, updateIdentifier);
    }

    private static Credential buildCredential(OMANode credNode) throws OMAException {
        long ctime = getTime(credNode.getChild(TAG_CreationDate));
        long expTime = getTime(credNode.getChild(TAG_ExpirationDate));
        String realm = getString(credNode.getChild(TAG_Realm));
        boolean checkAAACert = getBoolean(credNode.getChild(TAG_CheckAAAServerCertStatus));

        OMANode unNode = credNode.getChild(TAG_UsernamePassword);
        OMANode certNode = credNode.getChild(TAG_DigitalCertificate);
        OMANode simNode = credNode.getChild(TAG_SIM);

        int alternatives = 0;
        alternatives += unNode != null ? 1 : 0;
        alternatives += certNode != null ? 1 : 0;
        alternatives += simNode != null ? 1 : 0;
        if (alternatives != 1) {
            throw new OMAException("Expected exactly one credential type, got " + alternatives);
        }

        if (unNode != null) {
            String userName = getString(unNode.getChild(TAG_Username));
            String password = getString(unNode.getChild(TAG_Password));
            boolean machineManaged = getBoolean(unNode.getChild(TAG_MachineManaged));
            String softTokenApp = getString(unNode.getChild(TAG_SoftTokenApp));
            boolean ableToShare = getBoolean(unNode.getChild(TAG_AbleToShare));

            OMANode eapMethodNode = unNode.getChild(TAG_EAPMethod);
            int eapID = getInteger(eapMethodNode.getChild(TAG_EAPType));

            EAP.EAPMethodID eapMethodID = EAP.mapEAPMethod(eapID);
            if (eapMethodID == null) {
                throw new OMAException("Unknown EAP method: " + eapID);
            }

            Long vid = getOptionalInteger(eapMethodNode.getChild(TAG_VendorId));
            Long vtype = getOptionalInteger(eapMethodNode.getChild(TAG_VendorType));
            Long innerEAPType = getOptionalInteger(eapMethodNode.getChild(TAG_InnerEAPType));
            EAP.EAPMethodID innerEAPMethod = null;
            if (innerEAPType != null) {
                innerEAPMethod = EAP.mapEAPMethod(innerEAPType.intValue());
                if (innerEAPMethod == null) {
                    throw new OMAException("Bad inner EAP method: " + innerEAPType);
                }
            }

            Long innerVid = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorID));
            Long innerVtype = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorType));
            String innerNonEAPMethod = getString(eapMethodNode.getChild(TAG_InnerMethod));

            EAPMethod eapMethod;
            if (innerEAPMethod != null) {
                eapMethod = new EAPMethod(eapMethodID, new InnerAuthEAP(innerEAPMethod));
            } else if (vid != null) {
                eapMethod = new EAPMethod(eapMethodID,
                        new ExpandedEAPMethod(EAP.AuthInfoID.ExpandedEAPMethod,
                                vid.intValue(), vtype));
            } else if (innerVid != null) {
                eapMethod =
                        new EAPMethod(eapMethodID, new ExpandedEAPMethod(EAP.AuthInfoID
                                .ExpandedInnerEAPMethod, innerVid.intValue(), innerVtype));
            } else if (innerNonEAPMethod != null) {
                eapMethod = new EAPMethod(eapMethodID, new NonEAPInnerAuth(innerNonEAPMethod));
            } else {
                throw new OMAException("Incomplete set of EAP parameters");
            }

            return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, userName,
                    password, machineManaged, softTokenApp, ableToShare);
        }
        if (certNode != null) {
            try {
                String certTypeString = getString(certNode.getChild(TAG_CertificateType));
                byte[] fingerPrint = getOctets(certNode.getChild(TAG_CertSHA256Fingerprint));

                EAPMethod eapMethod = new EAPMethod(EAP.EAPMethodID.EAP_TLS, null);

                return new Credential(ctime, expTime, realm, checkAAACert, eapMethod,
                        Credential.mapCertType(certTypeString), fingerPrint);
            } catch (NumberFormatException nfe) {
                throw new OMAException("Bad hex string: " + nfe.toString());
            }
        }
        if (simNode != null) {
            try {
                IMSIParameter imsi = new IMSIParameter(getString(simNode.getChild(TAG_IMSI)));

                EAPMethod eapMethod =
                        new EAPMethod(EAP.mapEAPMethod(getInteger(simNode.getChild(TAG_EAPType))),
                                null);

                return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, imsi);
            } catch (IOException ioe) {
                throw new OMAException("Failed to parse IMSI: " + ioe);
            }
        }
        throw new OMAException("Missing credential parameters");
    }

    public static OMANode getChild(OMANode node, String key) throws OMAException {
        OMANode child = node.getChild(key);
        if (child == null) {
            throw new OMAException("No such node: " + key);
        }
        return child;
    }

    public static String getString(OMANode node, String key) throws OMAException {
        OMANode child = node.getChild(key);
        if (child == null) {
            throw new OMAException("Missing value for " + key);
        } else if (!child.isLeaf()) {
            throw new OMAException(key + " is not a leaf node");
        }
        return child.getValue();
    }

    public static long getLong(OMANode node, String key, Long dflt) throws OMAException {
        OMANode child = node.getChild(key);
        if (child == null) {
            if (dflt != null) {
                return dflt;
            } else {
                throw new OMAException("Missing value for " + key);
            }
        } else {
            if (!child.isLeaf()) {
                throw new OMAException(key + " is not a leaf node");
            }
            String value = child.getValue();
            try {
                long result = Long.parseLong(value);
                if (result < 0) {
                    throw new OMAException("Negative value for " + key);
                }
                return result;
            } catch (NumberFormatException nfe) {
                throw new OMAException("Value for " + key + " is non-numeric: " + value);
            }
        }
    }

    public static <T> T getSelection(OMANode node, String key) throws OMAException {
        OMANode child = node.getChild(key);
        if (child == null) {
            throw new OMAException("Missing value for " + key);
        } else if (!child.isLeaf()) {
            throw new OMAException(key + " is not a leaf node");
        }
        return getSelection(key, child.getValue());
    }

    public static <T> T getSelection(String key, String value) throws OMAException {
        if (value == null) {
            throw new OMAException("No value for " + key);
        }
        Map<String, Object> kvp = sSelectionMap.get(key);
        T result = (T) kvp.get(value.toLowerCase());
        if (result == null) {
            throw new OMAException("Invalid value '" + value + "' for " + key);
        }
        return result;
    }

    private static boolean getBoolean(OMANode boolNode) {
        return boolNode != null && Boolean.parseBoolean(boolNode.getValue());
    }

    public static String getString(OMANode stringNode) {
        return stringNode != null ? stringNode.getValue() : null;
    }

    private static int getInteger(OMANode intNode, int dflt) throws OMAException {
        if (intNode == null) {
            return dflt;
        }
        return getInteger(intNode);
    }

    private static int getInteger(OMANode intNode) throws OMAException {
        if (intNode == null) {
            throw new OMAException("Missing integer value");
        }
        try {
            return Integer.parseInt(intNode.getValue());
        } catch (NumberFormatException nfe) {
            throw new OMAException("Invalid integer: " + intNode.getValue());
        }
    }

    private static Long getMac(OMANode macNode) throws OMAException {
        if (macNode == null) {
            return null;
        }
        try {
            return Long.parseLong(macNode.getValue(), 16);
        } catch (NumberFormatException nfe) {
            throw new OMAException("Invalid MAC: " + macNode.getValue());
        }
    }

    private static Long getOptionalInteger(OMANode intNode) throws OMAException {
        if (intNode == null) {
            return null;
        }
        try {
            return Long.parseLong(intNode.getValue());
        } catch (NumberFormatException nfe) {
            throw new OMAException("Invalid integer: " + intNode.getValue());
        }
    }

    public static long getTime(OMANode timeNode) throws OMAException {
        if (timeNode == null) {
            return Utils.UNSET_TIME;
        }
        String timeText = timeNode.getValue();
        try {
            Date date = DTFormat.parse(timeText);
            return date.getTime();
        } catch (ParseException pe) {
            throw new OMAException("Badly formatted time: " + timeText);
        }
    }

    private static byte[] getOctets(OMANode octetNode) throws OMAException {
        if (octetNode == null) {
            throw new OMAException("Missing byte value");
        }
        return Utils.hexToBytes(octetNode.getValue());
    }
}
