/*
 * Copyright (C) 2021 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.imsserviceentitlement;

import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__POLLING;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__SMSOIP;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOLTE;
import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI;
import static com.android.imsserviceentitlement.ts43.Ts43Constants.EntitlementVersion.ENTITLEMENT_VERSION_EIGHT;

import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.AsyncTask;
import android.os.PersistableBundle;
import android.telephony.SubscriptionManager;
import android.util.Log;
import android.util.SparseArray;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;

import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration;
import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior;
import com.android.imsserviceentitlement.entitlement.EntitlementResult;
import com.android.imsserviceentitlement.job.JobManager;
import com.android.imsserviceentitlement.utils.ImsUtils;
import com.android.imsserviceentitlement.utils.MetricsLogger;
import com.android.imsserviceentitlement.utils.TelephonyUtils;

import java.time.Duration;

/**
 * The {@link JobService} for querying entitlement status in the background. The jobId is unique for
 * different subId + job combination, so can run the same job for different subIds w/o cancelling
 * each others. See {@link JobManager}.
 */
public class ImsEntitlementPollingService extends JobService {
    private static final String TAG = "IMSSE-ImsEntitlementPollingService";

    public static final ComponentName COMPONENT_NAME =
            ComponentName.unflattenFromString(
                    "com.android.imsserviceentitlement/.ImsEntitlementPollingService");

    private ImsEntitlementApi mImsEntitlementApi;

    /**
     * Cache job id associated {@link EntitlementPollingTask} objects for canceling once job be
     * canceled.
     */
    private final SparseArray<EntitlementPollingTask> mTasks = new SparseArray<>();

    @VisibleForTesting
    EntitlementPollingTask mOngoingTask;

    @Override
    @VisibleForTesting
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }

    @VisibleForTesting
    void injectImsEntitlementApi(ImsEntitlementApi imsEntitlementApi) {
        this.mImsEntitlementApi = imsEntitlementApi;
    }

    /** Enqueues a job to query entitlement status. */
    public static void enqueueJob(Context context, int subId, int retryCount) {
        JobManager.getInstance(
                context,
                COMPONENT_NAME,
                subId)
                .queryEntitlementStatusOnceNetworkReady(retryCount);
    }

    /** Enqueues a job to query entitlement status with delay. */
    private static void enqueueJobWithDelay(Context context, int subId, long delayInSeconds) {
        JobManager.getInstance(
                context,
                COMPONENT_NAME,
                subId)
                .queryEntitlementStatusOnceNetworkReady(0, Duration.ofSeconds(delayInSeconds));
    }

    @Override
    public boolean onStartJob(final JobParameters params) {
        PersistableBundle bundle = params.getExtras();
        int subId =
                bundle.getInt(
                        SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);

        int jobId = params.getJobId();
        Log.d(TAG, "onStartJob: " + jobId);

        // Ignore the job if the SIM be removed or swapped
        if (!JobManager.isValidJob(this, params)) {
            Log.d(TAG, "Stop for invalid job! " + jobId);
            return false;
        }

        // if the same job ID is scheduled again, the current one will be cancelled by platform and
        // #onStopJob will be called to removed the job.
        mOngoingTask = new EntitlementPollingTask(params, subId);
        mTasks.put(jobId, mOngoingTask);
        mOngoingTask.execute();
        return true;
    }

    @Override
    public boolean onStopJob(final JobParameters params) {
        int jobId = params.getJobId();
        Log.d(TAG, "onStopJob: " + jobId);
        EntitlementPollingTask task = mTasks.get(jobId);
        if (task != null) {
            task.cancel(true);
            mTasks.remove(jobId);
        }

        return true;
    }

    @VisibleForTesting
    class EntitlementPollingTask extends AsyncTask<Void, Void, Void> {
        private final JobParameters mParams;
        private final ImsEntitlementApi mImsEntitlementApi;
        private final ImsUtils mImsUtils;
        private final TelephonyUtils mTelephonyUtils;
        private final MetricsLogger mMetricsLogger;
        private final int mSubid;
        private final int mEntitlementVersion;
        private final boolean mNeedsImsProvisioning;

        // States for metrics
        private long mStartTime;
        private long mDurationMillis;
        private int mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE;
        private int mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
        private int mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
        private int mVonrResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
        private int mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;

        EntitlementPollingTask(final JobParameters params, int subId) {
            this.mParams = params;
            this.mImsUtils = ImsUtils.getInstance(ImsEntitlementPollingService.this, subId);
            this.mTelephonyUtils = new TelephonyUtils(ImsEntitlementPollingService.this, subId);
            this.mSubid = subId;
            this.mEntitlementVersion =
                    TelephonyUtils.getEntitlementVersion(ImsEntitlementPollingService.this, mSubid);
            this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired(
                    ImsEntitlementPollingService.this, mSubid);
            this.mImsEntitlementApi = ImsEntitlementPollingService.this.mImsEntitlementApi != null
                    ? ImsEntitlementPollingService.this.mImsEntitlementApi
                    : new ImsEntitlementApi(ImsEntitlementPollingService.this, subId);
            this.mMetricsLogger = new MetricsLogger(mTelephonyUtils);
        }

        @Override
        protected Void doInBackground(Void... unused) {
            int jobId = JobManager.getPureJobId(mParams.getJobId());
            switch (jobId) {
                case JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID:
                    mMetricsLogger.start(IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__POLLING);
                    doEntitlementCheck();
                    break;
                default:
                    break;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void unused) {
            Log.d(TAG, "JobId:" + mParams.getJobId() + "- Task done.");
            sendStatsLogToMetrics();
            ImsEntitlementPollingService.this.jobFinished(mParams, false);
        }

        @Override
        protected void onCancelled(Void unused) {
            sendStatsLogToMetrics();
        }

        private void doEntitlementCheck() {
            if (mNeedsImsProvisioning) {
                // TODO(b/190476343): Unify EntitlementResult and EntitlementConfiguration.
                doImsEntitlementCheck();
            } else {
                doWfcEntitlementCheck();
            }
        }

        @WorkerThread
        private void doImsEntitlementCheck() {
            try {
                EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus();
                Log.d(TAG, "Entitlement result: " + result);

                if (performRetryIfNeeded(result)) {
                    return;
                }

                if (shouldTurnOffWfc(result)) {
                    mImsUtils.setVowifiProvisioned(false);
                    mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
                } else {
                    mImsUtils.setVowifiProvisioned(true);
                    mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
                }

                if (shouldTurnOffVolte(result)) {
                    mImsUtils.setVolteProvisioned(false);
                    mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
                } else {
                    mImsUtils.setVolteProvisioned(true);
                    mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
                }

                if (mEntitlementVersion >= ENTITLEMENT_VERSION_EIGHT) {
                    if (shouldTurnOffVonrHome(result)) {
                        mImsUtils.setVonrProvisioned(false);
                        mVonrResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
                    } else {
                        mImsUtils.setVonrProvisioned(true);
                        mVonrResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
                    }
                }

                if (shouldTurnOffSMSoIP(result)) {
                    mImsUtils.setSmsoipProvisioned(false);
                    mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
                } else {
                    mImsUtils.setSmsoipProvisioned(true);
                    mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
                }
            } catch (RuntimeException e) {
                mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
                mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
                mVonrResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
                mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
                Log.d(TAG, "checkEntitlementStatus failed.", e);
            }
            checkVersValidity();
        }

        @WorkerThread
        private void doWfcEntitlementCheck() {
            if (!mImsUtils.isWfcEnabledByUser()) {
                Log.d(TAG, "WFC not turned on; checkEntitlementStatus not needed this time.");
                return;
            }
            try {
                EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus();
                Log.d(TAG, "Entitlement result: " + result);

                if (performRetryIfNeeded(result)) {
                    return;
                }

                if (shouldTurnOffWfc(result)) {
                    mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
                    mImsUtils.disableWfc();
                } else {
                    mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
                }
            } catch (RuntimeException e) {
                mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
                Log.d(TAG, "checkEntitlementStatus failed.", e);
            }
        }

        /**
         * Performs retry if needed. Returns true if {@link ImsEntitlementPollingService} has
         * scheduled.
         */
        private boolean performRetryIfNeeded(@Nullable EntitlementResult result) {
            if (result == null || result.getRetryAfterSeconds() < 0) {
                return false;
            }
            mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
            ImsEntitlementPollingService.enqueueJobWithDelay(
                    ImsEntitlementPollingService.this,
                    mSubid,
                    result.getRetryAfterSeconds());
            return true;
        }

        /**
         * Schedules entitlement status check after a VERS.validity time, if the last valid is
         * during validity.
         */
        private void checkVersValidity() {
            EntitlementConfiguration lastEntitlementConfiguration =
                    new EntitlementConfiguration(ImsEntitlementPollingService.this, mSubid);
            if (lastEntitlementConfiguration.entitlementValidation()
                    == ClientBehavior.VALID_DURING_VALIDITY) {
                enqueueJobWithDelay(
                        ImsEntitlementPollingService.this,
                        mSubid,
                        lastEntitlementConfiguration.getVersValidity());
            }
        }

        /**
         * Returns {@code true} when {@code EntitlementResult} says WFC is not activated; Otherwise
         * {@code false} if {@code EntitlementResult} is not of any known pattern.
         */
        private boolean shouldTurnOffWfc(@Nullable EntitlementResult result) {
            if (result == null) {
                Log.d(TAG, "Entitlement API failed to return a result; don't turn off WFC.");
                return false;
            }

            // Only turn off WFC for known patterns indicating WFC not activated.
            return result.getVowifiStatus().serverDataMissing()
                    || result.getVowifiStatus().inProgress()
                    || result.getVowifiStatus().incompatible();
        }

        private boolean shouldTurnOffVolte(@Nullable EntitlementResult result) {
            if (result == null) {
                Log.d(TAG, "Entitlement API failed to return a result; don't turn off VoLTE.");
                return false;
            }

            // Only turn off VoLTE for known patterns indicating VoLTE not activated.
            return !result.getVolteStatus().isActive();
        }

        private boolean shouldTurnOffVonrHome(@Nullable EntitlementResult result) {
            if (result == null) {
                Log.d(TAG, "Entitlement API failed to return a result; don't turn off VoNR.");
                return false;
            }

            // Only turn off VoNR in Home for known patterns indicating VoNR not activated.
            return !result.getVonrStatus().isHomeActive();
        }

        private boolean shouldTurnOffSMSoIP(@Nullable EntitlementResult result) {
            if (result == null) {
                Log.d(TAG, "Entitlement API failed to return a result; don't turn off SMSoIP.");
                return false;
            }

            // Only turn off SMSoIP for known patterns indicating SMSoIP not activated.
            return !result.getSmsoveripStatus().isActive();
        }

        private void sendStatsLogToMetrics() {
            // If no result set, it was cancelled for reasons.
            if (mVowifiResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
                mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
            }
            mMetricsLogger.write(
                    IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI, mVowifiResult);

            if (mNeedsImsProvisioning) {
                if (mVolteResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
                    mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
                }
                if (mSmsoipResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
                    mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
                }
                mMetricsLogger.write(
                        IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOLTE, mVolteResult);
                mMetricsLogger.write(
                        IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__SMSOIP, mSmsoipResult);
            }
        }

        @VisibleForTesting
        int getVonrResult() {
            return mVonrResult;
        }

        @VisibleForTesting
        int getVowifiResult() {
            return mVowifiResult;
        }
    }
}

