/*
 * Copyright (C) 2020 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 com.android.wifitrackerlib;

import static androidx.core.util.Preconditions.checkNotNull;

import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel;
import static com.android.wifitrackerlib.WifiEntry.ConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN;

import android.net.Uri;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.hotspot2.OsuProvider;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.net.wifi.hotspot2.ProvisioningCallback;
import android.os.Handler;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.os.BuildCompat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

/**
 * WifiEntry representation of an Online Sign-up entry, uniquely identified by FQDN.
 */
class OsuWifiEntry extends WifiEntry {
    static final String KEY_PREFIX = "OsuWifiEntry:";

    // Scan result list must be thread safe for generating the verbose scan summary
    @NonNull private final List<ScanResult> mCurrentScanResults = new ArrayList<>();

    @NonNull private final String mKey;
    @NonNull private final OsuProvider mOsuProvider;
    private String mSsid;
    private String mOsuStatusString;
    private boolean mIsAlreadyProvisioned = false;
    private boolean mHasAddConfigUserRestriction = false;
    private final UserManager mUserManager;

    /**
     * Create an OsuWifiEntry with the associated OsuProvider
     */
    OsuWifiEntry(
            @NonNull WifiTrackerInjector injector,
            @NonNull Handler callbackHandler,
            @NonNull OsuProvider osuProvider,
            @NonNull WifiManager wifiManager,
            boolean forSavedNetworksPage) throws IllegalArgumentException {
        super(injector, callbackHandler, wifiManager, forSavedNetworksPage);

        checkNotNull(osuProvider, "Cannot construct with null osuProvider!");

        mOsuProvider = osuProvider;
        mKey = osuProviderToOsuWifiEntryKey(osuProvider);
        mUserManager = injector.getUserManager();
        if (BuildCompat.isAtLeastT() && mUserManager != null) {
            mHasAddConfigUserRestriction = mUserManager.hasUserRestriction(
                    UserManager.DISALLOW_ADD_WIFI_CONFIG);
        }
    }

    @Override
    public String getKey() {
        return mKey;
    }

    @Override
    public synchronized String getTitle() {
        final String friendlyName = mOsuProvider.getFriendlyName();
        if (!TextUtils.isEmpty(friendlyName)) {
            return friendlyName;
        }
        if (!TextUtils.isEmpty(mSsid)) {
            return mSsid;
        }
        final Uri serverUri = mOsuProvider.getServerUri();
        if (serverUri != null) {
            return serverUri.toString();
        }
        return "";
    }

    @Override
    public synchronized String getSummary(boolean concise) {
        if (hasAdminRestrictions()) {
            return mContext.getString(R.string.wifitrackerlib_admin_restricted_network);
        }

        // TODO(b/70983952): Add verbose summary
        if (mOsuStatusString != null) {
            return mOsuStatusString;
        } else if (isAlreadyProvisioned()) {
            return concise ? mContext.getString(R.string.wifitrackerlib_wifi_passpoint_expired)
                    : mContext.getString(
                    R.string.wifitrackerlib_tap_to_renew_subscription_and_connect);
        } else {
            return mContext.getString(R.string.wifitrackerlib_tap_to_sign_up);
        }
    }

    @Override
    public synchronized String getSsid() {
        return mSsid;
    }

    @Override
    public String getMacAddress() {
        // TODO(b/70983952): Fill this method in in case we need the mac address for verbose logging
        return null;
    }

    @Override
    public synchronized boolean canConnect() {
        //check user restriction and whether the network is already provisioned
        if (hasAdminRestrictions()) {
            return false;
        }
        return mLevel != WIFI_LEVEL_UNREACHABLE
                && getConnectedState() == CONNECTED_STATE_DISCONNECTED;
    }

    @Override
    public synchronized void connect(@Nullable ConnectCallback callback) {
        mConnectCallback = callback;
        mWifiManager.stopRestrictingAutoJoinToSubscriptionId();
        mWifiManager.startSubscriptionProvisioning(mOsuProvider, mContext.getMainExecutor(),
                new OsuWifiEntryProvisioningCallback());
    }

    @WorkerThread
    synchronized void updateScanResultInfo(@Nullable List<ScanResult> scanResults)
            throws IllegalArgumentException {
        if (scanResults == null) scanResults = new ArrayList<>();

        mCurrentScanResults.clear();
        mCurrentScanResults.addAll(scanResults);

        final ScanResult bestScanResult = getBestScanResultByLevel(scanResults);
        if (bestScanResult != null) {
            mSsid = bestScanResult.SSID;
            if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
                mLevel = mWifiManager.calculateSignalLevel(bestScanResult.level);
            }
        } else {
            mLevel = WIFI_LEVEL_UNREACHABLE;
        }
        notifyOnUpdated();
    }

    @NonNull
    static String osuProviderToOsuWifiEntryKey(@NonNull OsuProvider osuProvider) {
        checkNotNull(osuProvider, "Cannot create key with null OsuProvider!");
        return KEY_PREFIX + osuProvider.getFriendlyName() + ","
                + osuProvider.getServerUri().toString();
    }

    @WorkerThread
    @Override
    protected boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo) {
        return wifiInfo.isOsuAp() && TextUtils.equals(
                wifiInfo.getPasspointProviderFriendlyName(), mOsuProvider.getFriendlyName());
    }

    @Override
    protected String getScanResultDescription() {
        // TODO(b/70983952): Fill this method in.
        return "";
    }

    OsuProvider getOsuProvider() {
        return mOsuProvider;
    }

    synchronized boolean isAlreadyProvisioned() {
        return mIsAlreadyProvisioned;
    }

    synchronized void setAlreadyProvisioned(boolean isAlreadyProvisioned) {
        mIsAlreadyProvisioned = isAlreadyProvisioned;
    }

    @Override
    public synchronized boolean hasAdminRestrictions() {
        if (mHasAddConfigUserRestriction && !mIsAlreadyProvisioned) {
            return true;
        }
        return false;
    }

    class OsuWifiEntryProvisioningCallback extends ProvisioningCallback {
        @Override
        @MainThread public void onProvisioningFailure(int status) {
            synchronized (OsuWifiEntry.this) {
                if (TextUtils.equals(
                        mOsuStatusString, mContext.getString(
                                R.string.wifitrackerlib_osu_completing_sign_up))) {
                    mOsuStatusString =
                            mContext.getString(R.string.wifitrackerlib_osu_sign_up_failed);
                } else {
                    mOsuStatusString =
                            mContext.getString(R.string.wifitrackerlib_osu_connect_failed);
                }
            }
            final ConnectCallback connectCallback = mConnectCallback;
            if (connectCallback != null) {
                connectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN);
            }
            notifyOnUpdated();
        }

        @Override
        @MainThread public void onProvisioningStatus(int status) {
            String newStatusString = null;
            switch (status) {
                case OSU_STATUS_AP_CONNECTING:
                case OSU_STATUS_AP_CONNECTED:
                case OSU_STATUS_SERVER_CONNECTING:
                case OSU_STATUS_SERVER_VALIDATED:
                case OSU_STATUS_SERVER_CONNECTED:
                case OSU_STATUS_INIT_SOAP_EXCHANGE:
                case OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE:
                    newStatusString = String.format(mContext.getString(
                            R.string.wifitrackerlib_osu_opening_provider),
                            getTitle());
                    break;
                case OSU_STATUS_REDIRECT_RESPONSE_RECEIVED:
                case OSU_STATUS_SECOND_SOAP_EXCHANGE:
                case OSU_STATUS_THIRD_SOAP_EXCHANGE:
                case OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS:
                    newStatusString = mContext.getString(
                    R.string.wifitrackerlib_osu_completing_sign_up);
                    break;
            }
            synchronized (OsuWifiEntry.this) {
                boolean updated = !TextUtils.equals(mOsuStatusString, newStatusString);
                mOsuStatusString = newStatusString;
                if (updated) {
                    notifyOnUpdated();
                }
            }
        }

        @Override
        @MainThread public void onProvisioningComplete() {
            synchronized (OsuWifiEntry.this) {
                mOsuStatusString = mContext.getString(R.string.wifitrackerlib_osu_sign_up_complete);
            }
            notifyOnUpdated();

            PasspointConfiguration passpointConfig = mWifiManager
                    .getMatchingPasspointConfigsForOsuProviders(Collections.singleton(mOsuProvider))
                    .get(mOsuProvider);
            final ConnectCallback connectCallback = mConnectCallback;
            if (passpointConfig == null) {
                // Failed to find the config we just provisioned
                if (connectCallback != null) {
                    connectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN);
                }
                return;
            }
            String uniqueId = passpointConfig.getUniqueId();
            for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pairing :
                    mWifiManager.getAllMatchingWifiConfigs(mWifiManager.getScanResults())) {
                WifiConfiguration config = pairing.first;
                if (TextUtils.equals(config.getKey(), uniqueId)) {
                    List<ScanResult> homeScans =
                            pairing.second.get(WifiManager.PASSPOINT_HOME_NETWORK);
                    List<ScanResult> roamingScans =
                            pairing.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK);
                    ScanResult bestScan;
                    if (homeScans != null && !homeScans.isEmpty()) {
                        bestScan = getBestScanResultByLevel(homeScans);
                    } else if (roamingScans != null && !roamingScans.isEmpty()) {
                        bestScan = getBestScanResultByLevel(roamingScans);
                    } else {
                        break;
                    }
                    config.SSID = "\"" + bestScan.SSID + "\"";
                    mWifiManager.connect(config, null /* ActionListener */);
                    return;
                }
            }

            // Failed to find the network we provisioned for
            if (connectCallback != null) {
                connectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN);
            }
        }
    }

    @Override
    public String toString() {
        StringJoiner sj = new StringJoiner("][", "[", "]");
        sj.add("FriendlyName:" + mOsuProvider.getFriendlyName());
        sj.add("ServerUri:" + mOsuProvider.getServerUri());
        sj.add("SSID:" + mSsid);
        return super.toString() + sj;
    }
}
