/**
 * Copyright (c) 2017, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.wifi.hotspot2.pps;

import android.net.wifi.ParcelUtil;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Objects;

/**
 * Class representing configuration parameters for subscription or policy update in
 * PerProviderSubscription (PPS) Management Object (MO) tree.  This is used by both
 * PerProviderSubscription/Policy/PolicyUpdate and PerProviderSubscription/SubscriptionUpdate
 * subtree.
 *
 * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
 * Release 2 Technical Specification.
 *
 * @hide
 */
public final class UpdateParameter implements Parcelable {
    private static final String TAG = "UpdateParameter";

    /**
     * Value indicating policy update is not applicable.  Thus, never check with policy server
     * for updates.
     */
    public static final long UPDATE_CHECK_INTERVAL_NEVER = 0xFFFFFFFFL;

    /**
     * Valid string for UpdateMethod.
     */
    public static final String UPDATE_METHOD_OMADM = "OMA-DM-ClientInitiated";
    public static final String UPDATE_METHOD_SPP = "SPP-ClientInitiated";

    /**
     * Valid string for Restriction.
     */
    public static final String UPDATE_RESTRICTION_HOMESP = "HomeSP";
    public static final String UPDATE_RESTRICTION_ROAMING_PARTNER = "RoamingPartner";
    public static final String UPDATE_RESTRICTION_UNRESTRICTED = "Unrestricted";

    /**
     * Maximum bytes for URI string.
     */
    private static final int MAX_URI_BYTES = 1023;

    /**
     * Maximum bytes for URI string.
     */
    private static final int MAX_URL_BYTES = 1023;

    /**
     * Maximum bytes for username.
     */
    private static final int MAX_USERNAME_BYTES = 63;

    /**
     * Maximum bytes for password.
     */
    private static final int MAX_PASSWORD_BYTES = 255;

    /**
     * Number of bytes for certificate SHA-256 fingerprint byte array.
     */
    private static final int CERTIFICATE_SHA256_BYTES = 32;

    /**
     * This specifies how often the mobile device shall check with policy server for updates.
     *
     * Using Long.MIN_VALUE to indicate unset value.
     */
    private long mUpdateIntervalInMinutes = Long.MIN_VALUE;
    public void setUpdateIntervalInMinutes(long updateIntervalInMinutes) {
        mUpdateIntervalInMinutes = updateIntervalInMinutes;
    }
    public long getUpdateIntervalInMinutes() {
        return mUpdateIntervalInMinutes;
    }

    /**
     * The method used to update the policy.  Permitted values are "OMA-DM-ClientInitiated"
     * and "SPP-ClientInitiated".
     */
    private String mUpdateMethod = null;
    public void setUpdateMethod(String updateMethod) {
        mUpdateMethod = updateMethod;
    }
    public String getUpdateMethod() {
        return mUpdateMethod;
    }

    /**
     * This specifies the hotspots at which the subscription update is permitted.  Permitted
     * values are "HomeSP", "RoamingPartner", or "Unrestricted";
     */
    private String mRestriction = null;
    public void setRestriction(String restriction) {
        mRestriction = restriction;
    }
    public String getRestriction() {
        return mRestriction;
    }

    /**
     * The URI of the update server.
     */
    private String mServerUri = null;
    public void setServerUri(String serverUri) {
        mServerUri = serverUri;
    }
    public String getServerUri() {
        return mServerUri;
    }

    /**
     * Username used to authenticate with the policy server.
     */
    private String mUsername = null;
    public void setUsername(String username) {
        mUsername = username;
    }
    public String getUsername() {
        return mUsername;
    }

    /**
     * Base64 encoded password used to authenticate with the policy server.
     */
    private String mBase64EncodedPassword = null;
    public void setBase64EncodedPassword(String password) {
        mBase64EncodedPassword = password;
    }
    public String getBase64EncodedPassword() {
        return mBase64EncodedPassword;
    }

    /**
     * HTTPS URL for retrieving certificate for trust root.  The trust root is used to validate
     * policy server's identity.
     */
    private String mTrustRootCertUrl = null;
    public void setTrustRootCertUrl(String trustRootCertUrl) {
        mTrustRootCertUrl = trustRootCertUrl;
    }
    public String getTrustRootCertUrl() {
        return mTrustRootCertUrl;
    }

    /**
     * SHA-256 fingerprint of the certificate located at {@code mTrustRootCertUrl}
     */
    private byte[] mTrustRootCertSha256Fingerprint = null;
    public void setTrustRootCertSha256Fingerprint(byte[] fingerprint) {
        mTrustRootCertSha256Fingerprint = fingerprint;
    }
    public byte[] getTrustRootCertSha256Fingerprint() {
        return mTrustRootCertSha256Fingerprint;
    }

    /**
     * CA (Certificate Authority) X509 certificates.
     */
    private X509Certificate mCaCertificate;

    /**
     * Set the CA (Certification Authority) certificate associated with Policy/Subscription update.
     *
     * @param caCertificate The CA certificate to set
     * @hide
     */
    public void setCaCertificate(X509Certificate caCertificate) {
        mCaCertificate = caCertificate;
    }

    /**
     * Get the CA (Certification Authority) certificate associated with Policy/Subscription update.
     *
     * @return CA certificate associated and {@code null} if certificate is not set.
     * @hide
     */
    public X509Certificate getCaCertificate() {
        return mCaCertificate;
    }

    /**
     * Constructor for creating Policy with default values.
     */
    public UpdateParameter() {}

    /**
     * Copy constructor.
     *
     * @param source The source to copy from
     */
    public UpdateParameter(UpdateParameter source) {
        if (source == null) {
            return;
        }
        mUpdateIntervalInMinutes = source.mUpdateIntervalInMinutes;
        mUpdateMethod = source.mUpdateMethod;
        mRestriction = source.mRestriction;
        mServerUri = source.mServerUri;
        mUsername = source.mUsername;
        mBase64EncodedPassword = source.mBase64EncodedPassword;
        mTrustRootCertUrl = source.mTrustRootCertUrl;
        if (source.mTrustRootCertSha256Fingerprint != null) {
            mTrustRootCertSha256Fingerprint = Arrays.copyOf(source.mTrustRootCertSha256Fingerprint,
                    source.mTrustRootCertSha256Fingerprint.length);
        }
        mCaCertificate = source.mCaCertificate;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(mUpdateIntervalInMinutes);
        dest.writeString(mUpdateMethod);
        dest.writeString(mRestriction);
        dest.writeString(mServerUri);
        dest.writeString(mUsername);
        dest.writeString(mBase64EncodedPassword);
        dest.writeString(mTrustRootCertUrl);
        dest.writeByteArray(mTrustRootCertSha256Fingerprint);
        ParcelUtil.writeCertificate(dest, mCaCertificate);
    }

    @Override
    public boolean equals(Object thatObject) {
        if (this == thatObject) {
            return true;
        }
        if (!(thatObject instanceof UpdateParameter)) {
            return false;
        }
        UpdateParameter that = (UpdateParameter) thatObject;

        return mUpdateIntervalInMinutes == that.mUpdateIntervalInMinutes
                && TextUtils.equals(mUpdateMethod, that.mUpdateMethod)
                && TextUtils.equals(mRestriction, that.mRestriction)
                && TextUtils.equals(mServerUri, that.mServerUri)
                && TextUtils.equals(mUsername, that.mUsername)
                && TextUtils.equals(mBase64EncodedPassword, that.mBase64EncodedPassword)
                && TextUtils.equals(mTrustRootCertUrl, that.mTrustRootCertUrl)
                && Arrays.equals(mTrustRootCertSha256Fingerprint,
                that.mTrustRootCertSha256Fingerprint)
                && Credential.isX509CertificateEquals(mCaCertificate, that.mCaCertificate);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mUpdateIntervalInMinutes, mUpdateMethod, mRestriction, mServerUri,
                mUsername, mBase64EncodedPassword, mTrustRootCertUrl,
                Arrays.hashCode(mTrustRootCertSha256Fingerprint), mCaCertificate);
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("UpdateInterval: ").append(mUpdateIntervalInMinutes).append("\n");
        builder.append("UpdateMethod: ").append(mUpdateMethod).append("\n");
        builder.append("Restriction: ").append(mRestriction).append("\n");
        builder.append("ServerURI: ").append(mServerUri).append("\n");
        builder.append("Username: ").append(mUsername).append("\n");
        builder.append("TrustRootCertURL: ").append(mTrustRootCertUrl).append("\n");
        return builder.toString();
    }

    /**
     * Validate UpdateParameter data.
     *
     * @return true on success
     * @hide
     */
    public boolean validate() {
        if (mUpdateIntervalInMinutes == Long.MIN_VALUE) {
            Log.d(TAG, "Update interval not specified");
            return false;
        }
        // Update not applicable.
        if (mUpdateIntervalInMinutes == UPDATE_CHECK_INTERVAL_NEVER) {
            return true;
        }

        if (!TextUtils.equals(mUpdateMethod, UPDATE_METHOD_OMADM)
                && !TextUtils.equals(mUpdateMethod, UPDATE_METHOD_SPP)) {
            Log.d(TAG, "Unknown update method: " + mUpdateMethod);
            return false;
        }

        if (!TextUtils.equals(mRestriction, UPDATE_RESTRICTION_HOMESP)
                && !TextUtils.equals(mRestriction, UPDATE_RESTRICTION_ROAMING_PARTNER)
                && !TextUtils.equals(mRestriction, UPDATE_RESTRICTION_UNRESTRICTED)) {
            Log.d(TAG, "Unknown restriction: " + mRestriction);
            return false;
        }

        if (TextUtils.isEmpty(mServerUri)) {
            Log.d(TAG, "Missing update server URI");
            return false;
        }
        if (mServerUri.getBytes(StandardCharsets.UTF_8).length > MAX_URI_BYTES) {
            Log.d(TAG, "URI bytes exceeded the max: "
                    + mServerUri.getBytes(StandardCharsets.UTF_8).length);
            return false;
        }

        if (TextUtils.isEmpty(mUsername)) {
            Log.d(TAG, "Missing username");
            return false;
        }
        if (mUsername.getBytes(StandardCharsets.UTF_8).length > MAX_USERNAME_BYTES) {
            Log.d(TAG, "Username bytes exceeded the max: "
                    + mUsername.getBytes(StandardCharsets.UTF_8).length);
            return false;
        }

        if (TextUtils.isEmpty(mBase64EncodedPassword)) {
            Log.d(TAG, "Missing username");
            return false;
        }
        if (mBase64EncodedPassword.getBytes(StandardCharsets.UTF_8).length > MAX_PASSWORD_BYTES) {
            Log.d(TAG, "Password bytes exceeded the max: "
                    + mBase64EncodedPassword.getBytes(StandardCharsets.UTF_8).length);
            return false;
        }
        try {
            Base64.decode(mBase64EncodedPassword, Base64.DEFAULT);
        } catch (IllegalArgumentException e) {
            Log.d(TAG, "Invalid encoding for password: " + mBase64EncodedPassword);
            return false;
        }

        if (TextUtils.isEmpty(mTrustRootCertUrl)) {
            Log.d(TAG, "Missing trust root certificate URL");
            return false;
        }
        if (mTrustRootCertUrl.getBytes(StandardCharsets.UTF_8).length > MAX_URL_BYTES) {
            Log.d(TAG, "Trust root cert URL bytes exceeded the max: "
                    + mTrustRootCertUrl.getBytes(StandardCharsets.UTF_8).length);
            return false;
        }

        if (mTrustRootCertSha256Fingerprint == null) {
            Log.d(TAG, "Missing trust root certificate SHA-256 fingerprint");
            return false;
        }
        if (mTrustRootCertSha256Fingerprint.length != CERTIFICATE_SHA256_BYTES) {
            Log.d(TAG, "Incorrect size of trust root certificate SHA-256 fingerprint: "
                    + mTrustRootCertSha256Fingerprint.length);
            return false;
        }
        return true;
    }

    public static final @android.annotation.NonNull Creator<UpdateParameter> CREATOR =
        new Creator<UpdateParameter>() {
            @Override
            public UpdateParameter createFromParcel(Parcel in) {
                UpdateParameter updateParam = new UpdateParameter();
                updateParam.setUpdateIntervalInMinutes(in.readLong());
                updateParam.setUpdateMethod(in.readString());
                updateParam.setRestriction(in.readString());
                updateParam.setServerUri(in.readString());
                updateParam.setUsername(in.readString());
                updateParam.setBase64EncodedPassword(in.readString());
                updateParam.setTrustRootCertUrl(in.readString());
                updateParam.setTrustRootCertSha256Fingerprint(in.createByteArray());
                updateParam.setCaCertificate(ParcelUtil.readCertificate(in));
                return updateParam;
            }

            @Override
            public UpdateParameter[] newArray(int size) {
                return new UpdateParameter[size];
            }
        };
}
