/*
 * 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;

import static android.adservices.ondevicepersonalization.OnDevicePersonalizationPermissions.NOTIFY_MEASUREMENT_EVENT;

import android.adservices.ondevicepersonalization.CallerMetadata;
import android.adservices.ondevicepersonalization.Constants;
import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
import android.adservices.ondevicepersonalization.aidl.IIsFeatureEnabledCallback;
import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService;
import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.os.Trace;

import com.android.internal.annotations.VisibleForTesting;
import com.android.odp.module.common.DeviceUtils;
import com.android.odp.module.common.ProcessWrapper;
import com.android.ondevicepersonalization.internal.util.LoggerFactory;
import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
import com.android.ondevicepersonalization.services.serviceflow.ServiceFlowOrchestrator;
import com.android.ondevicepersonalization.services.serviceflow.ServiceFlowType;
import com.android.ondevicepersonalization.services.statsd.ApiCallStats;
import com.android.ondevicepersonalization.services.statsd.OdpStatsdLogger;

import java.util.Objects;

/** Implementation of OnDevicePersonalizationManagingService */
public class OnDevicePersonalizationManagingServiceDelegate
        extends IOnDevicePersonalizationManagingService.Stub {
    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
    private static final String TAG = "OnDevicePersonalizationManagingServiceDelegate";
    private static final ServiceFlowOrchestrator sSfo = ServiceFlowOrchestrator.getInstance();
    @NonNull private final Context mContext;
    private final Injector mInjector;

    public OnDevicePersonalizationManagingServiceDelegate(@NonNull Context context) {
        this(context, new Injector());
    }

    @VisibleForTesting
    public OnDevicePersonalizationManagingServiceDelegate(
            @NonNull Context context, Injector injector) {
        mContext = Objects.requireNonNull(context);
        mInjector = injector;
    }

    static class Injector {
        Flags getFlags() {
            return FlagsFactory.getFlags();
        }
    }

    @Override
    public String getVersion() {
        return "1.0";
    }

    @Override
    public void execute(
            @NonNull String callingPackageName,
            @NonNull ComponentName handler,
            @NonNull Bundle wrappedParams,
            @NonNull CallerMetadata metadata,
            @NonNull ExecuteOptionsParcel options,
            @NonNull IExecuteCallback callback) {
        if (getGlobalKillSwitch()) {
            throw new IllegalStateException("Service skipped as the global kill switch is on.");
        }

        if (!DeviceUtils.isOdpSupported(mContext)) {
            throw new IllegalStateException("Device not supported.");
        }
        long serviceEntryTimeMillis = SystemClock.elapsedRealtime();

        Trace.beginSection("OdpManagingServiceDelegate#Execute");
        Objects.requireNonNull(callingPackageName);
        Objects.requireNonNull(handler);
        Objects.requireNonNull(handler.getPackageName());
        Objects.requireNonNull(handler.getClassName());
        Objects.requireNonNull(wrappedParams);
        Objects.requireNonNull(metadata);
        Objects.requireNonNull(callback);
        if (callingPackageName.isEmpty()) {
            throw new IllegalArgumentException("missing app package name");
        }
        if (handler.getPackageName().isEmpty()) {
            throw new IllegalArgumentException("missing service package name");
        }
        if (handler.getClassName().isEmpty()) {
            throw new IllegalArgumentException("missing service class name");
        }

        checkExecutionsOptions(options);

        final int uid = Binder.getCallingUid();
        enforceCallingPackageBelongsToUid(callingPackageName, uid);
        enforceEnrollment(callingPackageName, handler);

        sSfo.schedule(
                ServiceFlowType.APP_REQUEST_FLOW,
                callingPackageName,
                handler,
                wrappedParams,
                callback,
                mContext,
                metadata.getStartTimeMillis(),
                serviceEntryTimeMillis,
                options);
        Trace.endSection();
    }

    @Override
    public void requestSurfacePackage(
            @NonNull String slotResultToken,
            @NonNull IBinder hostToken,
            int displayId,
            int width,
            int height,
            @NonNull CallerMetadata metadata,
            @NonNull IRequestSurfacePackageCallback callback) {
        if (getGlobalKillSwitch()) {
            throw new IllegalStateException("Service skipped as the global kill switch is on.");
        }

        if (!DeviceUtils.isOdpSupported(mContext)) {
            throw new IllegalStateException("Device not supported.");
        }
        long serviceEntryTimeMillis = SystemClock.elapsedRealtime();

        Trace.beginSection("OdpManagingServiceDelegate#RequestSurfacePackage");
        Objects.requireNonNull(slotResultToken);
        Objects.requireNonNull(hostToken);
        Objects.requireNonNull(callback);
        if (width <= 0) {
            throw new IllegalArgumentException("width must be > 0");
        }

        if (height <= 0) {
            throw new IllegalArgumentException("height must be > 0");
        }

        if (displayId < 0) {
            throw new IllegalArgumentException("displayId must be >= 0");
        }

        sSfo.schedule(ServiceFlowType.RENDER_FLOW,
                slotResultToken, hostToken, displayId,
                width, height, callback,
                mContext, metadata.getStartTimeMillis(), serviceEntryTimeMillis);
        Trace.endSection();
    }

    // TODO(b/301732670): Move to a new service.
    @Override
    public void registerMeasurementEvent(
            @NonNull int measurementEventType,
            @NonNull Bundle params,
            @NonNull CallerMetadata metadata,
            @NonNull IRegisterMeasurementEventCallback callback
    ) {
        if (getGlobalKillSwitch()) {
            throw new IllegalStateException("Service skipped as the global kill switch is on.");
        }

        if (!DeviceUtils.isOdpSupported(mContext)) {
            throw new IllegalStateException("Device not supported.");
        }

        if (mContext.checkCallingPermission(NOTIFY_MEASUREMENT_EVENT)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Permission denied: " + NOTIFY_MEASUREMENT_EVENT);
        }

        long serviceEntryTimeMillis = SystemClock.elapsedRealtime();
        Trace.beginSection("OdpManagingServiceDelegate#RegisterMeasurementEvent");
        if (measurementEventType
                != Constants.MEASUREMENT_EVENT_TYPE_WEB_TRIGGER) {
            throw new IllegalStateException("invalid measurementEventType");
        }
        Objects.requireNonNull(params);
        Objects.requireNonNull(metadata);
        Objects.requireNonNull(callback);

        sSfo.schedule(ServiceFlowType.WEB_TRIGGER_FLOW,
                params, mContext,
                callback, metadata.getStartTimeMillis(), serviceEntryTimeMillis);
        Trace.endSection();
    }

    @Override
    public void isFeatureEnabled(
            @NonNull String featureName,
            @NonNull CallerMetadata metadata,
            @NonNull IIsFeatureEnabledCallback callback) {
        if (getGlobalKillSwitch()) {
            throw new IllegalStateException("Service skipped as the global kill switch is on.");
        }

        if (!DeviceUtils.isOdpSupported(mContext)) {
            throw new IllegalStateException("Device not supported.");
        }

        if (!getOdpIsFeatureEnabledFlagEnabled()) {
            throw new IllegalStateException("isFeatureEnabled flag is not enabled.");
        }

        long serviceEntryTimeMillis = SystemClock.elapsedRealtime();
        Trace.beginSection("OdpManagingServiceDelegate#IsFeatureEnabled");

        FeatureStatusManager.getFeatureStatusAndSendResult(featureName,
                serviceEntryTimeMillis,
                callback);

        Trace.endSection();
    }

    @Override
    public void logApiCallStats(
            String sdkPackageName, int apiName, long latencyMillis, long rpcCallLatencyMillis,
            long rpcReturnLatencyMillis, int responseCode) {
        final int uid = Binder.getCallingUid();
        OnDevicePersonalizationExecutors.getBackgroundExecutor()
                .execute(
                        () ->
                                handleLogApiCallStats(uid, sdkPackageName, apiName, latencyMillis,
                                        rpcCallLatencyMillis, rpcReturnLatencyMillis,
                                        responseCode));
    }

    private void handleLogApiCallStats(int appUid, String sdkPackageName, int apiName,
            long latencyMillis, long rpcCallLatencyMillis, long rpcReturnLatencyMillis,
            int responseCode) {
        try {
            OdpStatsdLogger.getInstance()
                    .logApiCallStats(
                            new ApiCallStats.Builder(apiName)
                                    .setResponseCode(responseCode)
                                    .setAppUid(appUid)
                                    .setSdkPackageName(sdkPackageName == null ? "" : sdkPackageName)
                                    .setLatencyMillis((int) latencyMillis)
                                    .setRpcCallLatencyMillis((int) rpcCallLatencyMillis)
                                    .setRpcReturnLatencyMillis((int) rpcReturnLatencyMillis)
                                    .build());
        } catch (Exception e) {
            sLogger.e(e, TAG + ": error logging api call stats");
        }
    }

    private boolean getGlobalKillSwitch() {
        long origId = Binder.clearCallingIdentity();
        boolean globalKillSwitch = mInjector.getFlags().getGlobalKillSwitch();
        Binder.restoreCallingIdentity(origId);
        return globalKillSwitch;
    }

    private boolean getOdpIsFeatureEnabledFlagEnabled() {
        long origId = Binder.clearCallingIdentity();
        boolean flagEnabled = mInjector.getFlags().isFeatureEnabledApiEnabled();
        Binder.restoreCallingIdentity(origId);
        return flagEnabled;
    }

    @VisibleForTesting
    void enforceCallingPackageBelongsToUid(@NonNull String packageName, int uid) {
        int packageUid;
        PackageManager pm = mContext.getPackageManager();
        try {
            packageUid = pm.getPackageUid(packageName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            throw new SecurityException(packageName + " not found");
        }

        int appUid = ProcessWrapper.isSdkSandboxUid(uid)
                ? ProcessWrapper.getAppUidForSdkSandboxUid(uid) : uid;
        if (packageUid != appUid) {
            throw new SecurityException(packageName + " does not belong to uid " + uid);
        }
    }

    private void enforceEnrollment(@NonNull String callingPackageName,
            @NonNull ComponentName service) {
        long origId = Binder.clearCallingIdentity();

        try {
            if (!PartnerEnrollmentChecker.isCallerAppEnrolled(callingPackageName)) {
                sLogger.d("caller app %s not enrolled to call ODP.", callingPackageName);
                throw new IllegalStateException(
                        "Service skipped as the caller app is not enrolled to call ODP.");
            }
            if (!PartnerEnrollmentChecker.isIsolatedServiceEnrolled(service.getPackageName())) {
                sLogger.d("isolated service %s not enrolled to access ODP.",
                        service.getPackageName());
                throw new IllegalStateException(
                        "Service skipped as the isolated service is not enrolled to access ODP.");
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

    private void checkExecutionsOptions(@NonNull ExecuteOptionsParcel options) {
        long origId = Binder.clearCallingIdentity();
        try {
            if (options.getOutputType()
                    == ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE
                    && options.getMaxIntValue() > mInjector.getFlags().getMaxIntValuesLimit()) {
                throw new IllegalArgumentException(
                        "The maxIntValue in OutputSpec can not exceed limit "
                                + mInjector.getFlags().getMaxIntValuesLimit());
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }
}