/*
 * Copyright 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 android.credentials;

import static android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.IntentSender;
import android.os.CancellationSignal;
import android.os.OutcomeReceiver;
import android.util.Log;

import java.util.concurrent.Executor;


/**
 * A response object that prefetches user app credentials and provides metadata about them. It can
 * then be used to issue the full credential retrieval flow via the
 * {@link CredentialManager#getCredential(PendingGetCredentialHandle, Activity, CancellationSignal,
 * Executor, OutcomeReceiver)} method to perform the remaining flows such as consent collection
 * and credential selection, to officially retrieve a credential.
 */
public final class PrepareGetCredentialResponse {

    /**
     * A handle that represents a pending get-credential operation. Pass this handle to {@link
     * CredentialManager#getCredential(PendingGetCredentialHandle, Activity, CancellationSignal,
     * Executor, OutcomeReceiver)} to perform the remaining flows to officially retrieve a
     * credential.
     */
    public static final class PendingGetCredentialHandle {
        @NonNull
        private final CredentialManager.GetCredentialTransportPendingUseCase
                mGetCredentialTransport;
        /**
         * The pending intent to be launched to finalize the user credential. If null, the callback
         * will fail with {@link GetCredentialException#TYPE_NO_CREDENTIAL}.
         */
        @Nullable
        private final PendingIntent mPendingIntent;

        /** @hide */
        PendingGetCredentialHandle(
                @NonNull CredentialManager.GetCredentialTransportPendingUseCase transport,
                @Nullable PendingIntent pendingIntent) {
            mGetCredentialTransport = transport;
            mPendingIntent = pendingIntent;
        }

        /** @hide */
        void show(@NonNull Context context, @Nullable CancellationSignal cancellationSignal,
                @CallbackExecutor @NonNull Executor executor,
                @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
            if (mPendingIntent == null) {
                executor.execute(() -> callback.onError(
                        new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL)));
                return;
            }

            mGetCredentialTransport.setCallback(new GetPendingCredentialInternalCallback() {
                @Override
                public void onPendingIntent(PendingIntent pendingIntent) {
                    try {
                        context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
                    } catch (IntentSender.SendIntentException e) {
                        Log.e(TAG, "startIntentSender() failed for intent for show()", e);
                        executor.execute(() -> callback.onError(
                                new GetCredentialException(GetCredentialException.TYPE_UNKNOWN)));
                    }
                }

                @Override
                public void onResponse(GetCredentialResponse response) {
                    executor.execute(() -> callback.onResult(response));
                }

                @Override
                public void onError(String errorType, String message) {
                    executor.execute(
                            () -> callback.onError(new GetCredentialException(errorType, message)));
                }
            });

            try {
                context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0);
            } catch (IntentSender.SendIntentException e) {
                Log.e(TAG, "startIntentSender() failed for intent for show()", e);
                executor.execute(() -> callback.onError(
                        new GetCredentialException(GetCredentialException.TYPE_UNKNOWN)));
            }
        }
    }
    private static final String TAG = "CredentialManager";

    @NonNull private final PrepareGetCredentialResponseInternal mResponseInternal;

    @NonNull private final PendingGetCredentialHandle mPendingGetCredentialHandle;

    /**
     * Returns true if the user has any candidate credentials for the given {@code credentialType},
     * and false otherwise.
     */
    @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
    public boolean hasCredentialResults(@NonNull String credentialType) {
        return mResponseInternal.hasCredentialResults(credentialType);
    }

    /**
     * Returns true if the user has any candidate authentication actions (locked credential
     * supplier), and false otherwise.
     */
    @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
    public boolean hasAuthenticationResults() {
        return mResponseInternal.hasAuthenticationResults();
    }

    /**
     * Returns true if the user has any candidate remote credential results, and false otherwise.
     */
    @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
    public boolean hasRemoteResults() {
        return mResponseInternal.hasRemoteResults();
    }

    /**
     * Returns a handle that represents this pending get-credential operation. Pass this handle to
     * {@link CredentialManager#getCredential(PendingGetCredentialHandle, Activity,
     * CancellationSignal, Executor, OutcomeReceiver)} to perform the remaining flows to officially
     * retrieve a credential.
     */
    @NonNull
    public PendingGetCredentialHandle getPendingGetCredentialHandle() {
        return mPendingGetCredentialHandle;
    }

    /**
     * Constructs a {@link PrepareGetCredentialResponse}.
     *
     * @param responseInternal       whether caller has the permission to query the credential
     *                               result metadata
     * @param getCredentialTransport the transport for the operation to finalaze a credential
     * @hide
     */
    protected PrepareGetCredentialResponse(
            @NonNull PrepareGetCredentialResponseInternal responseInternal,
            @NonNull CredentialManager.GetCredentialTransportPendingUseCase
                    getCredentialTransport) {
        mResponseInternal = responseInternal;
        mPendingGetCredentialHandle = new PendingGetCredentialHandle(
                getCredentialTransport, responseInternal.getPendingIntent());
    }

    /** @hide */
    protected interface GetPendingCredentialInternalCallback {
        void onPendingIntent(@NonNull PendingIntent pendingIntent);

        void onResponse(@NonNull GetCredentialResponse response);

        void onError(@NonNull String errorType, @Nullable String message);
    }
}