package com.android.server.wifi.hotspot2.pps;

import android.util.Log;

import com.android.server.wifi.SIMAccessor;
import com.android.server.wifi.anqp.ANQPElement;
import com.android.server.wifi.anqp.CellularNetwork;
import com.android.server.wifi.anqp.DomainNameElement;
import com.android.server.wifi.anqp.NAIRealmElement;
import com.android.server.wifi.anqp.RoamingConsortiumElement;
import com.android.server.wifi.anqp.ThreeGPPNetworkElement;
import com.android.server.wifi.hotspot2.AuthMatch;
import com.android.server.wifi.hotspot2.NetworkDetail;
import com.android.server.wifi.hotspot2.PasspointMatch;
import com.android.server.wifi.hotspot2.Utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.android.server.wifi.anqp.Constants.ANQPElementType;

public class HomeSP {
    private final Map<String, Long> mSSIDs;        // SSID, HESSID, [0,N]
    private final String mFQDN;
    private final DomainMatcher mDomainMatcher;
    private final Set<String> mOtherHomePartners;
    private final HashSet<Long> mRoamingConsortiums;    // [0,N]
    private final Set<Long> mMatchAnyOIs;           // [0,N]
    private final List<Long> mMatchAllOIs;          // [0,N]

    private final Credential mCredential;

    // Informational:
    private final String mFriendlyName;             // [1]
    private final String mIconURL;                  // [0,1]

    public HomeSP(Map<String, Long> ssidMap,
                   /*@NotNull*/ String fqdn,
                   /*@NotNull*/ HashSet<Long> roamingConsortiums,
                   /*@NotNull*/ Set<String> otherHomePartners,
                   /*@NotNull*/ Set<Long> matchAnyOIs,
                   /*@NotNull*/ List<Long> matchAllOIs,
                   String friendlyName,
                   String iconURL,
                   Credential credential) {

        mSSIDs = ssidMap;
        List<List<String>> otherPartners = new ArrayList<>(otherHomePartners.size());
        for (String otherPartner : otherHomePartners) {
            otherPartners.add(Utils.splitDomain(otherPartner));
        }
        mOtherHomePartners = otherHomePartners;
        mFQDN = fqdn;
        mDomainMatcher = new DomainMatcher(Utils.splitDomain(fqdn), otherPartners);
        mRoamingConsortiums = roamingConsortiums;
        mMatchAnyOIs = matchAnyOIs;
        mMatchAllOIs = matchAllOIs;
        mFriendlyName = friendlyName;
        mIconURL = iconURL;
        mCredential = credential;
    }

    public HomeSP getClone(String password) {
        if (getCredential().hasDisregardPassword()) {
            return new HomeSP(mSSIDs,
                    mFQDN,
                    mRoamingConsortiums,
                    mOtherHomePartners,
                    mMatchAnyOIs,
                    mMatchAllOIs,
                    mFriendlyName,
                    mIconURL,
                    new Credential(mCredential, password));
        }
        else {
            return this;
        }
    }

    public PasspointMatch match(NetworkDetail networkDetail,
                                Map<ANQPElementType, ANQPElement> anqpElementMap,
                                SIMAccessor simAccessor) {

        List<String> imsis = simAccessor.getMatchingImsis(mCredential.getImsi());

        PasspointMatch spMatch = matchSP(networkDetail, anqpElementMap, imsis);

        if (spMatch == PasspointMatch.Incomplete || spMatch == PasspointMatch.Declined) {
            return spMatch;
        }

        if (imsiMatch(imsis, (ThreeGPPNetworkElement)
                anqpElementMap.get(ANQPElementType.ANQP3GPPNetwork)) != null) {
            // PLMN match, promote sp match to roaming if necessary.
            return spMatch == PasspointMatch.None ? PasspointMatch.RoamingProvider : spMatch;
        }

        NAIRealmElement naiRealmElement =
                (NAIRealmElement) anqpElementMap.get(ANQPElementType.ANQPNAIRealm);

        int authMatch = naiRealmElement != null ?
                naiRealmElement.match(mCredential) :
                AuthMatch.Indeterminate;
        
        Log.d(Utils.hs2LogTag(getClass()), networkDetail.toKeyString() + " match on " + mFQDN +
                ": " + spMatch + ", auth " + AuthMatch.toString(authMatch));

        if (authMatch == AuthMatch.None) {
            // Distinct auth mismatch, demote authentication.
            return PasspointMatch.None;
        }
        else if ((authMatch & AuthMatch.Realm) == 0) {
            // No realm match, return sp match as is.
            return spMatch;
        }
        else {
            // Realm match, promote sp match to roaming if necessary.
            return spMatch == PasspointMatch.None ? PasspointMatch.RoamingProvider : spMatch;
        }
    }

    public PasspointMatch matchSP(NetworkDetail networkDetail,
                                Map<ANQPElementType, ANQPElement> anqpElementMap,
                                List<String> imsis) {

        if (mSSIDs.containsKey(networkDetail.getSSID())) {
            Long hessid = mSSIDs.get(networkDetail.getSSID());
            if (hessid == null || networkDetail.getHESSID() == hessid) {
                Log.d(Utils.hs2LogTag(getClass()), "match SSID");
                return PasspointMatch.HomeProvider;
            }
        }

        Set<Long> anOIs = new HashSet<>();

        if (networkDetail.getRoamingConsortiums() != null) {
            for (long oi : networkDetail.getRoamingConsortiums()) {
                anOIs.add(oi);
            }
        }
        RoamingConsortiumElement rcElement = anqpElementMap != null ?
                (RoamingConsortiumElement) anqpElementMap.get(ANQPElementType.ANQPRoamingConsortium)
                : null;
        if (rcElement != null) {
            anOIs.addAll(rcElement.getOIs());
        }

        // It may seem reasonable to check for home provider match prior to checking for roaming
        // relationship, but it is possible to avoid an ANQP query if it turns out that the
        // "match all" rule fails based only on beacon info only.
        boolean roamingMatch = false;

        if (!mMatchAllOIs.isEmpty()) {
            boolean matchesAll = true;

            for (long spOI : mMatchAllOIs) {
                if (!anOIs.contains(spOI)) {
                    matchesAll = false;
                    break;
                }
            }
            if (matchesAll) {
                roamingMatch = true;
            }
            else {
                if (anqpElementMap != null || networkDetail.getAnqpOICount() == 0) {
                    return PasspointMatch.Declined;
                }
                else {
                    return PasspointMatch.Incomplete;
                }
            }
        }

        if (!roamingMatch &&
                (!Collections.disjoint(mMatchAnyOIs, anOIs) ||
                        !Collections.disjoint(mRoamingConsortiums, anOIs))) {
            roamingMatch = true;
        }

        if (anqpElementMap == null) {
            return PasspointMatch.Incomplete;
        }

        DomainNameElement domainNameElement =
                (DomainNameElement) anqpElementMap.get(ANQPElementType.ANQPDomName);

        if (domainNameElement != null) {
            for (String domain : domainNameElement.getDomains()) {
                List<String> anLabels = Utils.splitDomain(domain);
                DomainMatcher.Match match = mDomainMatcher.isSubDomain(anLabels);
                if (match != DomainMatcher.Match.None) {
                    return PasspointMatch.HomeProvider;
                }

                if (imsiMatch(imsis, anLabels) != null) {
                    return PasspointMatch.HomeProvider;
                }
            }
        }

        return roamingMatch ? PasspointMatch.RoamingProvider : PasspointMatch.None;
    }

    private String imsiMatch(List<String> imsis, ThreeGPPNetworkElement plmnElement) {
        if (imsis == null || plmnElement == null || plmnElement.getPlmns().isEmpty()) {
            return null;
        }
        for (CellularNetwork network : plmnElement.getPlmns()) {
            for (String mccMnc : network) {
                String imsi = imsiMatch(imsis, mccMnc);
                if (imsi != null) {
                    return imsi;
                }
            }
        }
        return null;
    }

    private String imsiMatch(List<String> imsis, List<String> fqdn) {
        if (imsis == null) {
            return null;
        }
        String mccMnc = Utils.getMccMnc(fqdn);
        return mccMnc != null ? imsiMatch(imsis, mccMnc) : null;
    }

    private String imsiMatch(List<String> imsis, String mccMnc) {
        if (mCredential.getImsi().matchesMccMnc(mccMnc)) {
            for (String imsi : imsis) {
                if (imsi.startsWith(mccMnc)) {
                    return imsi;
                }
            }
        }
        return null;
    }

    public String getFQDN() { return mFQDN; }
    public String getFriendlyName() { return mFriendlyName; }
    public HashSet<Long> getRoamingConsortiums() { return mRoamingConsortiums; }
    public Credential getCredential() { return mCredential; }

    public Map<String, Long> getSSIDs() {
        return mSSIDs;
    }

    public Collection<String> getOtherHomePartners() {
        return mOtherHomePartners;
    }

    public Set<Long> getMatchAnyOIs() {
        return mMatchAnyOIs;
    }

    public List<Long> getMatchAllOIs() {
        return mMatchAllOIs;
    }

    public String getIconURL() {
        return mIconURL;
    }

    public boolean deepEquals(HomeSP other) {
        return mFQDN.equals(other.mFQDN) &&
                mSSIDs.equals(other.mSSIDs) &&
                mOtherHomePartners.equals(other.mOtherHomePartners) &&
                mRoamingConsortiums.equals(other.mRoamingConsortiums) &&
                mMatchAnyOIs.equals(other.mMatchAnyOIs) &&
                mMatchAllOIs.equals(other.mMatchAllOIs) &&
                mFriendlyName.equals(other.mFriendlyName) &&
                Utils.compare(mIconURL, other.mIconURL) == 0 &&
                mCredential.equals(other.mCredential);
    }

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

        HomeSP that = (HomeSP) thatObject;
        return mFQDN.equals(that.mFQDN);
    }

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

    @Override
    public String toString() {
        return "HomeSP{" +
                "mSSIDs=" + mSSIDs +
                ", mFQDN='" + mFQDN + '\'' +
                ", mDomainMatcher=" + mDomainMatcher +
                ", mRoamingConsortiums={" + Utils.roamingConsortiumsToString(mRoamingConsortiums) +
                '}' +
                ", mMatchAnyOIs={" + Utils.roamingConsortiumsToString(mMatchAnyOIs) + '}' +
                ", mMatchAllOIs={" + Utils.roamingConsortiumsToString(mMatchAllOIs) + '}' +
                ", mCredential=" + mCredential +
                ", mFriendlyName='" + mFriendlyName + '\'' +
                ", mIconURL='" + mIconURL + '\'' +
                '}';
    }
}
