/*
 * Copyright (C) 2025 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.settings.accessibility;

import static com.android.internal.accessibility.common.NotificationConstants.EXTRA_PAGE_ID;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.os.PersistableBundle;
import android.util.Slog;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.server.accessibility.Flags;
import com.android.settings.accessibility.notification.NotificationHelper;

/**
 * JobService implementation for scheduling a notification to inform users about an accessibility
 * survey and guide them to review it.
 *
 * <p>This service ensures that the notification is displayed after a specified delay.
 */
public class AccessibilitySurveyNotificationJobService extends JobService {

    public static final String TAG = "AccessibilitySurveyNotificationJobService";

    // The base ID for job is derived from the bug component ID.
    // Page-specific notification IDs are generated by adding the respective pageId to this base.
    private static final int JOB_ID_BASE = 751131;

    @Nullable
    private NotificationHelper mNotificationHelper;

    /**
     * Default constructor for {@link AccessibilitySurveyNotificationJobService}.
     */
    public AccessibilitySurveyNotificationJobService() {}

    /**
     * Constructor for {@link AccessibilitySurveyNotificationJobService} for testing purposes.
     *
     * @param notificationHelper The {@link NotificationHelper} instance to use, or {@code null} if
     * it should be initialized lazily.
     */
    @VisibleForTesting
    public AccessibilitySurveyNotificationJobService(
            @Nullable NotificationHelper notificationHelper) {
        mNotificationHelper = notificationHelper;
    }

    /**
     * Schedule a new job that will show a notification after the specified amount of time.
     * If a job with the same ID is already pending, a new one will not be scheduled.
     *
     * @param context The application context.
     * @param pageId The identifier for the survey page to be shown.
     * @param delayMillis The minimum time in milliseconds to wait before showing the notification.
     */
    public void scheduleJob(@NonNull Context context, int pageId, long delayMillis) {
        if (!Flags.enableLowVisionHats()) {
            return;
        }

        final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
        if (jobScheduler == null) {
            return;
        }

        int jobId = getJobId(pageId);
        if (jobScheduler.getPendingJob(jobId) == null) {
            final ComponentName component = new ComponentName(
                    context, AccessibilitySurveyNotificationJobService.class);

            final PersistableBundle bundle = new PersistableBundle();
            bundle.putInt(EXTRA_PAGE_ID, pageId);

            final JobInfo newJob = new JobInfo.Builder(jobId, component)
                    .setMinimumLatency(delayMillis)
                    .setExtras(bundle)
                    .build();

            int scheduleResult = jobScheduler.schedule(newJob);
            if (scheduleResult == JobScheduler.RESULT_SUCCESS) {
                Slog.i(TAG, "Job successfully scheduled for pageId: " + pageId + " to run "
                        + "after " + delayMillis + "ms.");
            } else {
                Slog.e(TAG, "Job scheduling failed for pageId: " + pageId + " with result code: "
                        + scheduleResult);
            }
        }
    }

    /**
     * Cancels a previously scheduled job.
     *
     * @param context The application context.
     * @param pageId The identifier for the survey page whose job is to be cancelled.
     */
    public void cancelJob(@NonNull Context context, int pageId) {
        final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
        if (jobScheduler == null) {
            return;
        }

        int jobId = getJobId(pageId);
        if (jobScheduler.getPendingJob(jobId) != null) {
            jobScheduler.cancel(jobId);
        }
    }

    @Override
    public boolean onStartJob(@NonNull JobParameters params) {
        final PersistableBundle extras = params.getExtras();
        if (extras == null) {
            return false;
        }

        int pageId = extras.getInt(EXTRA_PAGE_ID, SettingsEnums.PAGE_UNKNOWN);
        if (pageId == SettingsEnums.PAGE_UNKNOWN) {
            return false;
        }

        Slog.i(TAG, "Job starting for pageId: " + pageId);
        if (mNotificationHelper == null) {
            mNotificationHelper = new NotificationHelper(this);
        }
        mNotificationHelper.showSurveyNotification(pageId);

        // Returning false here indicates that the job is finished immediately. The system will
        // release the wakelock, and onStopJob will not be invoked.
        return false;
    }

    @Override
    public boolean onStopJob(@NonNull JobParameters params) {
        Slog.w(TAG, "Job stopped by system. Job ID: " + params.getJobId() + ", reason: "
                + params.getStopReason());
        // Called if the system stops the job, e.g., due to preemption before or during
        // onStartJob's brief execution (as onStartJob returns false quickly).
        // Returning true attempts to reschedule the job with its original criteria.
        return true;
    }

    /**
     * Generates a job ID based on a base ID and a page ID. This ensures that job for different
     * pages will have distinct IDs.
     *
     * @param pageId The unique identifier of the page for which to generate a job ID.
     * @return A unique integer representing the job ID for the given page.
     */
    @VisibleForTesting
    public int getJobId(int pageId) {
        return JOB_ID_BASE + pageId;
    }
}
