/*
 * Copyright (C) 2024 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 android.app.appfunctions;

import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT;
import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_ENABLED;
import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_INDEXER_PACKAGE;
import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_ENABLED_BY_DEFAULT;
import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;

import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.GlobalSearchSession;
import android.app.appsearch.JoinSpec;
import android.app.appsearch.PropertyPath;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResults;
import android.app.appsearch.SearchSpec;
import android.os.OutcomeReceiver;
import android.text.TextUtils;

import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Helper class containing utilities for {@link AppFunctionManager}.
 *
 * @hide
 */
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
public class AppFunctionManagerHelper {

    /**
     * Returns (through a callback) a boolean indicating whether the app function is enabled.
     *
     * This method can only check app functions owned by the caller, or those where the caller
     * has visibility to the owner package and holds the {@link
     * Manifest.permission#EXECUTE_APP_FUNCTIONS} permission.
     *
     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
     *
     * <ul>
     *   <li>{@link AppFunctionNotFoundException}, if the function is not found or the caller does
     *       not have access to it.
     * </ul>
     *
     * @param functionIdentifier the identifier of the app function to check (unique within the
     *                           target package) and in most cases, these are automatically
     *                           generated by the AppFunctions
     *                           SDK
     * @param targetPackage      the package name of the app function's owner
     * @param executor           executor the executor to run the request
     * @param callback           the callback to receive the function enabled check result
     * @hide
     */
    public static void isAppFunctionEnabled(
            @NonNull String functionIdentifier,
            @NonNull String targetPackage,
            @NonNull AppSearchManager appSearchManager,
            @NonNull Executor executor,
            @NonNull OutcomeReceiver<Boolean, Exception> callback) {
        Objects.requireNonNull(functionIdentifier);
        Objects.requireNonNull(targetPackage);
        Objects.requireNonNull(appSearchManager);
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        appSearchManager.createGlobalSearchSession(
                executor,
                (searchSessionResult) -> {
                    if (!searchSessionResult.isSuccess()) {
                        callback.onError(failedResultToException(searchSessionResult));
                        return;
                    }
                    try (GlobalSearchSession searchSession = searchSessionResult.getResultValue()) {
                        SearchResults results =
                                searchJoinedStaticWithRuntimeAppFunctions(
                                        Objects.requireNonNull(searchSession),
                                        targetPackage,
                                        functionIdentifier);
                        results.getNextPage(
                                executor,
                                listAppSearchResult -> {
                                    if (listAppSearchResult.isSuccess()) {
                                        callback.onResult(
                                                getEffectiveEnabledStateFromSearchResults(
                                                        Objects.requireNonNull(
                                                                listAppSearchResult
                                                                        .getResultValue())));
                                    } else {
                                        callback.onError(
                                                failedResultToException(listAppSearchResult));
                                    }
                                });
                        results.close();
                    } catch (Exception e) {
                        callback.onError(e);
                    }
                });
    }

    /**
     * Searches joined app function static and runtime metadata using the function Id and the
     * package.
     */
    private static @NonNull SearchResults searchJoinedStaticWithRuntimeAppFunctions(
            @NonNull GlobalSearchSession session,
            @NonNull String targetPackage,
            @NonNull String functionIdentifier) {
        SearchSpec runtimeSearchSpec =
                getAppFunctionRuntimeMetadataSearchSpecByPackageName(targetPackage);
        JoinSpec joinSpec =
                new JoinSpec.Builder(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID)
                        .setNestedSearch(
                                buildFilerRuntimeMetadataByFunctionIdQuery(functionIdentifier),
                                runtimeSearchSpec)
                        .build();
        SearchSpec joinedStaticWithRuntimeSearchSpec =
                new SearchSpec.Builder()
                        .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
                        .addFilterSchemas(
                                AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage(
                                        targetPackage))
                        .addProjectionPaths(
                                SearchSpec.SCHEMA_TYPE_WILDCARD,
                                List.of(new PropertyPath(STATIC_PROPERTY_ENABLED_BY_DEFAULT)))
                        .setJoinSpec(joinSpec)
                        .setVerbatimSearchEnabled(true)
                        .build();
        return session.search(
                buildFilerStaticMetadataByFunctionIdQuery(functionIdentifier),
                joinedStaticWithRuntimeSearchSpec);
    }

    /**
     * Returns whether the function is effectively enabled or not from the search results returned
     * by {@link #searchJoinedStaticWithRuntimeAppFunctions}.
     *
     * @param joinedStaticRuntimeResults search results joining AppFunctionStaticMetadata
     *                                   and AppFunctionRuntimeMetadata.
     * @throws IllegalArgumentException if the function is not found in the results
     */
    private static boolean getEffectiveEnabledStateFromSearchResults(
            @NonNull List<SearchResult> joinedStaticRuntimeResults) {
        if (joinedStaticRuntimeResults.isEmpty()) {
            throw new IllegalArgumentException("App function not found.");
        } else {
            List<SearchResult> runtimeMetadataResults =
                    joinedStaticRuntimeResults.getFirst().getJoinedResults();
            if (runtimeMetadataResults.isEmpty()) {
                throw new IllegalArgumentException("App function not found.");
            }
            long enabled =
                    runtimeMetadataResults
                            .getFirst()
                            .getGenericDocument()
                            .getPropertyLong(PROPERTY_ENABLED);
            // If enabled is not equal to APP_FUNCTION_STATE_DEFAULT, it means it IS overridden and
            // we should return the overridden value.
            if (enabled != APP_FUNCTION_STATE_DEFAULT) {
                return enabled == APP_FUNCTION_STATE_ENABLED;
            }
            // Runtime metadata not found or enabled is equal to APP_FUNCTION_STATE_DEFAULT.
            // Using the default value in the static metadata.
            return joinedStaticRuntimeResults
                    .getFirst()
                    .getGenericDocument()
                    .getPropertyBoolean(STATIC_PROPERTY_ENABLED_BY_DEFAULT);
        }
    }

    /**
     * Returns search spec that queries app function metadata for a specific package name by its
     * function identifier.
     */
    private static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByPackageName(
            @NonNull String targetPackage) {
        return new SearchSpec.Builder()
                .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
                .addFilterSchemas(
                        AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage))
                .setVerbatimSearchEnabled(true)
                .build();
    }

    private static String buildFilerRuntimeMetadataByFunctionIdQuery(String functionIdentifier) {
        return TextUtils.formatSimple("%s:\"%s\"",
                AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
                functionIdentifier);
    }

    private static String buildFilerStaticMetadataByFunctionIdQuery(String functionIdentifier) {
        return TextUtils.formatSimple("%s:\"%s\"",
                AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID,
                functionIdentifier);
    }

    /** Converts a failed app search result codes into an exception. */
    private static @NonNull Exception failedResultToException(
            @NonNull AppSearchResult appSearchResult) {
        return switch (appSearchResult.getResultCode()) {
            case AppSearchResult.RESULT_INVALID_ARGUMENT -> new AppFunctionNotFoundException(
                    appSearchResult.getErrorMessage());
            case AppSearchResult.RESULT_IO_ERROR -> new IOException(
                    appSearchResult.getErrorMessage());
            case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException(
                    appSearchResult.getErrorMessage());
            default -> new IllegalStateException(appSearchResult.getErrorMessage());
        };
    }

    /**
     * Throws when the app function is not found.
     *
     * @hide
     */
    public static class AppFunctionNotFoundException extends RuntimeException {
        private AppFunctionNotFoundException(@NonNull String errorMessage) {
            super(errorMessage);
        }
    }
}
