/*
 * Copyright (C) 2022 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.ondevicepersonalization.services.download.mdd;

import static android.app.job.JobScheduler.RESULT_SUCCESS;

import static com.android.adservices.shared.proto.JobPolicy.BatteryType.BATTERY_TYPE_REQUIRE_NOT_LOW;
import static com.android.adservices.shared.proto.JobPolicy.NetworkType.NETWORK_TYPE_ANY;
import static com.android.adservices.shared.proto.JobPolicy.NetworkType.NETWORK_TYPE_NONE;
import static com.android.adservices.shared.proto.JobPolicy.NetworkType.NETWORK_TYPE_UNMETERED;
import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED;
import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED;
import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL;
import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID;
import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_CHARGING_PERIODIC_TASK_JOB_ID;
import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID;
import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID;

import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.PersistableBundle;

import com.android.adservices.shared.proto.JobPolicy;
import com.android.adservices.shared.proto.JobPolicy.NetworkType;
import com.android.adservices.shared.spe.JobServiceConstants;
import com.android.adservices.shared.spe.scheduling.JobSpec;
import com.android.internal.annotations.VisibleForTesting;
import com.android.ondevicepersonalization.internal.util.LoggerFactory;
import com.android.ondevicepersonalization.services.FlagsFactory;
import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobScheduler;
import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobServiceFactory;

import com.google.android.libraries.mobiledatadownload.TaskScheduler;

/**
 * MddTaskScheduler that uses JobScheduler to schedule MDD background tasks
 */
public class MddTaskScheduler implements TaskScheduler {
    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
    private static final String TAG = MddTaskScheduler.class.getSimpleName();
    private static final String MDD_TASK_SHARED_PREFS = "mdd_worker_task_periods";
    private final Context mContext;
    static final String MDD_NETWORK_STATE_KEY = "MDD_NETWORK_STATE_KEY";
    static final String MDD_PERIOD_SECONDS_KEY = "MDD_PERIOD_SECONDS_KEY";
    static final String MDD_TASK_TAG_KEY = "MDD_TASK_TAG_KEY";

    public MddTaskScheduler(Context context) {
        this.mContext = context;
    }

    private static int getMddTaskJobId(String mddTag) {
        switch (mddTag) {
            case MAINTENANCE_PERIODIC_TASK:
                return MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID;
            case CHARGING_PERIODIC_TASK:
                return MDD_CHARGING_PERIODIC_TASK_JOB_ID;
            case CELLULAR_CHARGING_PERIODIC_TASK:
                return MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID;
            default:
                return MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID;
        }
    }

    // Maps from the MDD-supplied NetworkState to the JobInfo equivalent int code.
    static int getNetworkConstraints(NetworkState networkState) {
        switch (networkState) {
            case NETWORK_STATE_ANY:
                // Network not required.
                return JobInfo.NETWORK_TYPE_NONE;
            case NETWORK_STATE_CONNECTED:
                // Metered or unmetered network available.
                return JobInfo.NETWORK_TYPE_ANY;
            case NETWORK_STATE_UNMETERED:
            default:
                return JobInfo.NETWORK_TYPE_UNMETERED;
        }
    }

    @Override
    public void schedulePeriodicTask(
            String mddTaskTag, long periodSeconds, NetworkState networkState) {
        schedule(mContext, mddTaskTag, periodSeconds, networkState);
    }

    /** Schedules a unique instance of {@link MddJob}. */
    public static void schedule(Context context, PersistableBundle extras) {
        schedule(context,
                getMddTaskTag(extras),
                getMddPeriodSeconds(extras),
                getMddNetworkState(extras));
    }

    /** Schedules a unique instance of {@link MddJobService}. */
    @JobServiceConstants.JobSchedulingResultCode
    public static int scheduleWithLegacy(
            Context context, PersistableBundle extras, boolean forceSchedule) {
        return scheduleWithLegacy(context,
                getMddTaskTag(extras),
                getMddPeriodSeconds(extras),
                getMddNetworkState(extras),
                forceSchedule);
    }

    private static void schedule(Context context, String mddTaskTag, long periodSeconds,
            NetworkState networkState) {
        if (FlagsFactory.getFlags().getSpeOnMddJobEnabled()) {
            OdpJobScheduler.getInstance(context).schedule(
                    context,
                    createJobSpec(mddTaskTag, periodSeconds, networkState));
            return;
        }

        sLogger.d("SPE is not enabled. Schedule the job with MddJobService.");
        int resultCode = scheduleWithLegacy(
                context, mddTaskTag, periodSeconds, networkState, /* forceSchedule */ false);

        OdpJobServiceFactory.getInstance(context)
                .getJobSchedulingLogger()
                .recordOnSchedulingLegacy(getMddTaskJobId(mddTaskTag), resultCode);
    }

    /** Schedules a unique instance of {@link MddJobService}. */
    @VisibleForTesting
    @JobServiceConstants.JobSchedulingResultCode
    static int scheduleWithLegacy(Context context, String mddTaskTag, long periodSeconds,
            NetworkState networkState, boolean forceSchedule) {
        SharedPreferences prefs =
                context.getSharedPreferences(MDD_TASK_SHARED_PREFS, Context.MODE_PRIVATE);

        // When the period change, we will need to update the existing works.
        boolean updateCurrent = false;
        if (getCurrentPeriodValue(prefs, mddTaskTag) != periodSeconds) {
            SharedPreferences.Editor editor = prefs.edit();
            editor.putLong(mddTaskTag, periodSeconds);
            editor.apply();
            updateCurrent = true;
        }

        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
        if (jobScheduler.getPendingJob(getMddTaskJobId(mddTaskTag)) == null) {
            sLogger.d(TAG + ": MddJob %s is not scheduled, scheduling now", mddTaskTag);
            return schedulePeriodicTaskWithUpdate(
                    context , jobScheduler, mddTaskTag, periodSeconds, networkState);
        } else if (updateCurrent) {
            sLogger.d(TAG + ": scheduling MddJob %s with frequency update", mddTaskTag);
            return schedulePeriodicTaskWithUpdate(
                    context, jobScheduler, mddTaskTag, periodSeconds, networkState);
        } else if (forceSchedule) {
            sLogger.d(TAG + ": force scheduling MddJob %s", mddTaskTag);
            return schedulePeriodicTaskWithUpdate(
                    context, jobScheduler, mddTaskTag, periodSeconds, networkState);
        }
        sLogger.d(TAG + ": MddJob %s already scheduled and frequency unchanged,"
                + " not scheduling", mddTaskTag);
        return SCHEDULING_RESULT_CODE_SKIPPED;
    }

    @JobServiceConstants.JobSchedulingResultCode
    private static int schedulePeriodicTaskWithUpdate(Context context, JobScheduler jobScheduler,
            String mddTaskTag, long periodSeconds, NetworkState networkState) {
        // We use extras to pass MDD config values. They will be used in the mdd jobs.
        PersistableBundle extras = new PersistableBundle();
        extras.putString(MDD_TASK_TAG_KEY, mddTaskTag);
        extras.putLong(MDD_PERIOD_SECONDS_KEY, periodSeconds);
        extras.putString(MDD_NETWORK_STATE_KEY, networkState.name());

        final JobInfo job =
                new JobInfo.Builder(
                        getMddTaskJobId(mddTaskTag),
                        new ComponentName(context, MddJobService.class))
                        .setRequiresDeviceIdle(true)
                        .setRequiresCharging(false)
                        .setRequiresBatteryNotLow(true)
                        .setPeriodic(1000 * periodSeconds) // JobScheduler uses Milliseconds.
                        .setRequiresStorageNotLow(requireStorageNotLow(mddTaskTag))
                        // persist this job across boots
                        .setPersisted(true)
                        .setRequiredNetworkType(getNetworkConstraints(networkState))
                        .setExtras(extras)
                        .build();
        int schedulingResult = jobScheduler.schedule(job);
        return RESULT_SUCCESS == schedulingResult ? SCHEDULING_RESULT_CODE_SUCCESSFUL
                : SCHEDULING_RESULT_CODE_FAILED;
    }

    @VisibleForTesting
    static JobSpec createJobSpec(String mddTaskTag, long periodSeconds, NetworkState networkState) {
        // We use extras to pass MDD config values. They will be used in the mdd jobs.
        PersistableBundle extras = new PersistableBundle();
        extras.putString(MDD_TASK_TAG_KEY, mddTaskTag);
        extras.putLong(MDD_PERIOD_SECONDS_KEY, periodSeconds);
        extras.putString(MDD_NETWORK_STATE_KEY, networkState.name());

        JobPolicy jobPolicy =
                JobPolicy.newBuilder()
                        .setJobId(getMddTaskJobId(mddTaskTag))
                        .setRequireDeviceIdle(true)
                        .setBatteryType(BATTERY_TYPE_REQUIRE_NOT_LOW)
                        .setPeriodicJobParams(
                                JobPolicy.PeriodicJobParams.newBuilder()
                                        .setPeriodicIntervalMs(1000 * periodSeconds)
                                        .build())
                        .setNetworkType(getNetworkType(networkState))
                        .setRequireStorageNotLow(requireStorageNotLow(mddTaskTag))
                        .setIsPersisted(true)
                        .build();

        return new JobSpec.Builder(jobPolicy)
                .setExtras(extras).build();
    }

    private static NetworkType getNetworkType(NetworkState networkState) {
        switch (networkState) {
            case NETWORK_STATE_ANY:
                // Network not required.
                return NETWORK_TYPE_NONE;
            case NETWORK_STATE_CONNECTED:
                // Metered or unmetered network available.
                return NETWORK_TYPE_ANY;
            case NETWORK_STATE_UNMETERED:
            default:
                return NETWORK_TYPE_UNMETERED;
        }
    }

    static String getMddTaskTag(final PersistableBundle extras) {
        requireNonNullExtras(extras);
        String mddTaskTag = extras.getString(MDD_TASK_TAG_KEY);
        if (null == mddTaskTag) {
            throw new IllegalArgumentException("Mdd task tag not found");
        }
        return mddTaskTag;
    }

    private static long getMddPeriodSeconds(final PersistableBundle extras) {
        requireNonNullExtras(extras);
        return extras.getLong(MDD_PERIOD_SECONDS_KEY);
    }

    private static NetworkState getMddNetworkState(final PersistableBundle extras) {
        requireNonNullExtras(extras);
        String networkState = extras.getString(MDD_NETWORK_STATE_KEY);
        if (networkState == null) {
            throw new IllegalArgumentException("MDD extra network state not found");
        }
        return NetworkState.valueOf(networkState);
    }

    private static void requireNonNullExtras(PersistableBundle extras) {
        if (null == extras) {
            throw new IllegalArgumentException("MDD extras not found");
        }
    }

    private static long getCurrentPeriodValue(SharedPreferences prefs, String mddTaskTag) {
        try {
            return prefs.getLong(mddTaskTag, 0);
        } catch (ClassCastException e) {
            sLogger.w(e, TAG + ": ClassCastException retrieving long value from prefs for tag: %s",
                    mddTaskTag);
            return 0;
        }
    }

    private static boolean requireStorageNotLow(String mddTaskTag) {
        return WIFI_CHARGING_PERIODIC_TASK.equals(mddTaskTag)
                || CELLULAR_CHARGING_PERIODIC_TASK.equals(mddTaskTag);
    }
}
