/*
 *  Copyright 2017 Google Inc. All Rights Reserved.
 *
 *  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.fido.example.fido2apiexample;

import android.content.Context;
import android.util.Log;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.fido.fido2.api.common.Attachment;
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse;
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse;
import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria;
import com.google.android.gms.fido.fido2.api.common.EC2Algorithm;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialType;
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.extensions.android.json.AndroidJsonFactory;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.services.AbstractGoogleClientRequest;
import com.google.api.client.googleapis.services.GoogleClientRequestInitializer;
import com.google.common.collect.FluentIterable;
import com.google.common.io.BaseEncoding;
import com.google.webauthn.gaedemo.fido2RequestHandler.Fido2RequestHandler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/** Service to communicate with WebAuthn demo server. */
public class GAEService {
    private static GAEService gaeService = null;
    private static final String TAG = "GAEService";

    private static final String KEY_REQUEST_CHALLENGE = "challenge";
    private static final String KEY_RP = "rp";
    private static final String KEY_RP_ID = "id";
    private static final String KEY_RP_NAME = "name";
    private static final String KEY_RP_ICON = "icon";
    private static final String KEY_USER = "user";
    private static final String KEY_USER_DISPLAY_NAME = "displayName";
    private static final String KEY_PARAMETERS = "pubKeyCredParams";
    private static final String KEY_PARAMETERS_TYPE = "type";
    private static final String KEY_TIMEOUT = "timeout";
    private static final String KEY_ATTACHMENT = "attachment";
    private static final String KEY_SESSION = "session";
    private static final String KEY_SESSION_ID = "id";
    private static final String KEY_RPID = "rpId";
    private static final String KEY_CLIENT_DATA_JSON = "clientDataJSON";
    private static final String KEY_ATTESTATION_OBJECT = "attestationObject";
    private static final String KEY_AUTHENTICATOR_DATA = "authenticatorData";
    private static final String KEY_CREDENTIAL_ID = "credentialId";
    private static final String KEY_SIGNATURE = "signature";

    private Fido2RequestHandler fido2Service;
    private final Context context;
    private final GoogleSignInAccount googleSignInAccount;
    private final Map<String, String> sessionIds;
    private List<Map<String, String>> securityTokens;

    public static GAEService getInstance(Context context, GoogleSignInAccount googleSignInAccount) {
        if (gaeService == null) {
            gaeService = new GAEService(context, googleSignInAccount);
        }
        return gaeService;
    }

    private GAEService(Context context, GoogleSignInAccount googleSignInAccount) {
        this.context = context;
        this.googleSignInAccount = googleSignInAccount;
        sessionIds = new HashMap<>();
        initFido2GAEService();
    }

    public PublicKeyCredentialCreationOptions getRegistrationRequest(List<String> excludedKeys) {
        try {
            if (fido2Service == null) {
                return null;
            }
            List<String> registerRequestContent =
                    fido2Service.getRegistrationRequest().execute().getItems();
            if (registerRequestContent == null || registerRequestContent.isEmpty()) {
                Log.i(TAG, "registerRequestContent is null or empty");
                return null;
            }
            for (String value : registerRequestContent) {
                Log.i(TAG, "registerRequestContent " + value);
            }
            // A sample register request:
            // {"rp":{"id":"webauthndemo.appspot.com","name":"webauthndemo.appspot.com"},
            //
            // "user":{"name":"littlecattest","displayName":"littlecattest","id":"bGl0dGxlY2F0dGVzdEBnbWFpbC5jb20="},
            //     "challenge":"Zys9NEvoE6KRhZtVMJZ3KKHg+spgXu2R0R7AQ2Mudlg=",
            //     "pubKeyCredParams":[{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},
            //     {"type":"public-key","alg":-36},{"type":"public-key","alg":-40},
            //     {"type":"public-key","alg":-41},{"type":"public-key","alg":-42},
            //     {"type":"public-key","alg":-37},{"type":"public-key","alg":-38},
            //     {"type":"public-key","alg":-39}],
            //     "session":{"id":5634387206995968,
            //     "challenge":"Zys9NEvoE6KRhZtVMJZ3KKHg+spgXu2R0R7AQ2Mudlg=",
            //     "origin":"webauthndemo.appspot.com"}}*/

            JSONObject registerRequestJson = new JSONObject(registerRequestContent.get(0));
            PublicKeyCredentialCreationOptions.Builder builder =
                    new PublicKeyCredentialCreationOptions.Builder();

            // Parse challenge
            builder.setChallenge(
                    BaseEncoding.base64().decode(registerRequestJson.getString(KEY_REQUEST_CHALLENGE)));

            // Parse RP
            JSONObject rpJson = registerRequestJson.getJSONObject(KEY_RP);
            String rpId = rpJson.getString(KEY_RP_ID);
            String rpName = rpJson.getString(KEY_RP_NAME);
            String rpIcon = null;
            if (rpJson.has(KEY_RP_ICON)) {
                rpIcon = rpJson.getString(KEY_RP_ICON);
            }
            PublicKeyCredentialRpEntity entity = new PublicKeyCredentialRpEntity(rpId, rpName, rpIcon);
            builder.setRp(entity);

            // Parse user
            JSONObject userJson = registerRequestJson.getJSONObject(KEY_USER);
            String displayName = userJson.getString(KEY_USER_DISPLAY_NAME);
            PublicKeyCredentialUserEntity userEntity =
                    new PublicKeyCredentialUserEntity(
                            displayName.getBytes() /* id */,
                            displayName /* name */,
                            null /* icon */,
                            displayName);
            builder.setUser(userEntity);

            // Parse parameters
            List<PublicKeyCredentialParameters> parameters = new ArrayList<>();
            if (registerRequestJson.has(KEY_PARAMETERS)) {
                JSONArray params = registerRequestJson.getJSONArray(KEY_PARAMETERS);
                for (int i = 0; i < params.length(); i++) {
                    JSONObject param = params.getJSONObject(i);
                    String type = param.getString(KEY_PARAMETERS_TYPE);
                    // TODO: this is a hack, use KEY_PARAMETERS_ALGORITHM = "alg"
                    PublicKeyCredentialParameters parameter =
                            new PublicKeyCredentialParameters(type, EC2Algorithm.ES256.getAlgoValue());
                    parameters.add(parameter);
                }
            }
            builder.setParameters(parameters);

            // Parse timeout
            Double timeout = null;
            if (registerRequestJson.has(KEY_TIMEOUT)) {
                timeout = Double.valueOf(registerRequestJson.getLong(KEY_TIMEOUT));
            }
            builder.setTimeoutSeconds(timeout);

            // Parse exclude list
            List<PublicKeyCredentialDescriptor> descriptors =
                    FluentIterable.from(excludedKeys)
                            .transform(
                                    k ->
                                            new PublicKeyCredentialDescriptor(
                                                    PublicKeyCredentialType.PUBLIC_KEY.toString(),
                                                    BaseEncoding.base64Url().decode(k),
                                                    /* transports= */ null))
                            .toList();
            builder.setExcludeList(descriptors);

            AuthenticatorSelectionCriteria.Builder criteria =
                    new AuthenticatorSelectionCriteria.Builder();
            if (registerRequestJson.has(KEY_ATTACHMENT)) {
                criteria.setAttachment(
                        Attachment.fromString(registerRequestJson.getString(KEY_ATTACHMENT)));
            }
            builder.setAuthenticatorSelection(criteria.build());
            return builder.build();
        } catch (IOException
                | JSONException
                | Attachment.UnsupportedAttachmentException e) {
            Log.e(TAG, "Error extracting information from server's registration request", e);
        }
        return null;
    }

    public String getRegisterResponseFromServer(AuthenticatorAttestationResponse response) {
        Log.d(TAG, "getRegisterResponseFromServer");
        try {
            if (fido2Service == null) {
                return null;
            }
            JSONObject responseJson = new JSONObject();
            String clientDataJSON = new String(response.getClientDataJSON(), "UTF-8");
            String attestationObject = BaseEncoding.base64().encode(response.getAttestationObject());
            responseJson.put(KEY_CLIENT_DATA_JSON, clientDataJSON);
            responseJson.put(KEY_ATTESTATION_OBJECT, attestationObject);

            List<String> registerResponseContent =
                    fido2Service.processRegistrationResponse(responseJson.toString()).execute().getItems();
            if (registerResponseContent == null || registerResponseContent.isEmpty()) {
                Log.i(TAG, "registerResponseContent is null or empty");
            } else {
                Log.i(TAG, "registerResponseContent " + registerResponseContent.get(0));
                JSONObject credential = new JSONObject(registerResponseContent.get(0));
                // return string value of the registered credential
                return credential.toString();
            }

        } catch (IOException | JSONException e) {
            Log.e(TAG, "Error processing registration response", e);
        }
        return null;
    }

    public PublicKeyCredentialRequestOptions getSignRequest(List<String> allowedKeys) {
        Log.d(TAG, "getSignRequest");
        try {
            if (fido2Service == null) {
                return null;
            }
            List<String> signRequestContent = fido2Service.getSignRequest().execute().getItems();
            if (signRequestContent == null || signRequestContent.isEmpty()) {
                Log.i(TAG, "signRequestContent is empty");
                return null;
            }
            for (String signRequest : signRequestContent) {
                Log.i(TAG, "signRequestContent " + signRequest);
            }
            JSONObject signRequestJson = new JSONObject(signRequestContent.get(0));
            PublicKeyCredentialRequestOptions.Builder builder =
                    new PublicKeyCredentialRequestOptions.Builder();
            // signRequestContent {"challenge":"AmlL6aQKTMd24MmfZtrvBGP/oKb8+zpXRcB7bfUHrPk=",
            // "rpId":"https://webauthdemo.appspot.com",
            // "allowList":[{"type":"public-key",
            // "id":"lmKQSq81f+gLQ49jeBQNFD/3TU7R2gGFWin+zNzpDrFeWUTTkEZ7nfmIC5OWXarRNqLxImA0hE7UVOI3eeVZZg=="}],
            // "session":{"id":5704837555552256,
            // "challenge":"AmlL6aQKTMd24MmfZtrvBGP/oKb8+zpXRcB7bfUHrPk=",
            // "origin":"https://webauthdemo.appspot.com"}}

            // Parse challenge
            builder.setChallenge(
                    BaseEncoding.base64().decode(signRequestJson.getString(KEY_REQUEST_CHALLENGE)));

            // Parse timeout
            if (signRequestJson.has(KEY_TIMEOUT)) {
                Double timeout = Double.valueOf(signRequestJson.getLong(KEY_TIMEOUT));
                builder.setTimeoutSeconds(timeout);
            }

            // Parse rpId
            String rpId = signRequestJson.getString(KEY_RPID);
            builder.setRpId(rpId);

            // Parse session id
            JSONObject session = signRequestJson.getJSONObject(KEY_SESSION);
            String sessionId = String.valueOf(session.getLong(KEY_SESSION_ID));

            // Parse allow list
            List<PublicKeyCredentialDescriptor> descriptors = new ArrayList<>();
            for (String allowedKey : allowedKeys) {
                sessionIds.put(allowedKey, sessionId);
                PublicKeyCredentialDescriptor publicKeyCredentialDescriptor =
                        new PublicKeyCredentialDescriptor(
                                PublicKeyCredentialType.PUBLIC_KEY.toString(),
                                BaseEncoding.base64Url().decode(allowedKey),
                                /* transports= */ null);
                descriptors.add(publicKeyCredentialDescriptor);
            }
            builder.setAllowList(descriptors);

            return builder.build();

        } catch (IOException | JSONException e) {
            Log.e(TAG, "Error processing sign request from server", e);
        }
        return null;
    }

    public String getSignResponseFromServer(AuthenticatorAssertionResponse response) {
        Log.d(TAG, "getSignResponseFromServer");
        try {
            if (fido2Service == null) {
                return null;
            }
            JSONObject responseJson = new JSONObject();
            String clientDataJSON = new String(response.getClientDataJSON(), "UTF-8");
            String authenticatorData = BaseEncoding.base64().encode(response.getAuthenticatorData());
            String credentialId = BaseEncoding.base64Url().encode(response.getKeyHandle());
            String signature = BaseEncoding.base64().encode(response.getSignature());
            responseJson.put(KEY_CLIENT_DATA_JSON, clientDataJSON);
            responseJson.put(KEY_AUTHENTICATOR_DATA, authenticatorData);
            responseJson.put(KEY_CREDENTIAL_ID, credentialId);
            responseJson.put(KEY_SIGNATURE, signature);

            // insert sessionId for the authenticated credential ID into result data in JSON format,
            // and pass it back to server.
            String sessionId = sessionIds.get(BaseEncoding.base64Url().encode(response.getKeyHandle()));
            responseJson.put(KEY_SESSION_ID, sessionId);

            List<String> signResponseContent =
                    fido2Service.processSignResponse(responseJson.toString()).execute().getItems();
            if (signResponseContent == null || signResponseContent.isEmpty()) {
                Log.i(TAG, "signResponseContent is null or empty");
            } else {
                Log.i(TAG, "signResponseContent " + signResponseContent.get(0));
                JSONObject credential = new JSONObject(signResponseContent.get(0));
                // return string value of the authenticated credential
                return credential.toString();
            }

        } catch (IOException | JSONException e) {
            Log.e(TAG, "Error processing sign response", e);
        }

        return null;
    }

    public List<Map<String, String>> getAllSecurityTokens() {
        Log.d(TAG, "getAllSecurityKeyTokens is called");
        try {
            if (fido2Service == null) {
                return new ArrayList<>();
            }
            List<String> securityKeyResponse = fido2Service.getAllSecurityKeys().execute().getItems();
            if (securityKeyResponse == null || securityKeyResponse.isEmpty()) {
                Log.i(TAG, "securityKeyResponse is null or empty");
                return null;
            }
            Log.i(TAG, "securityKeyResponse " + securityKeyResponse.get(0));
            securityTokens = new ArrayList<>();
            JSONArray tokenJsonArray = new JSONArray(securityKeyResponse.get(0));
            for (int i = 0; i < tokenJsonArray.length(); i++) {
                Map<String, String> tokenContent = new HashMap<>();

                JSONObject tokenJson = tokenJsonArray.getJSONObject(i);
                Iterator<String> keys = tokenJson.keys();
                while (keys.hasNext()) {
                    String key = keys.next();
                    tokenContent.put(key, tokenJson.getString(key));
                }
                securityTokens.add(tokenContent);
            }
            return securityTokens;

        } catch (IOException | JSONException e) {
            Log.e(TAG, "Error getting all security key tokens", e);
        }
        return null;
    }

    public String removeSecurityKey(String publicKey) {
        try {
            fido2Service.removeSecurityKey(publicKey);
            return publicKey;
        } catch (IOException e) {
            Log.e(TAG, "Error removing security key.", e);
            return null;
        }
    }

    private void initFido2GAEService() {
        if (fido2Service != null) {
            return;
        }
        if (googleSignInAccount == null) {
            return;
        }
        GoogleAccountCredential credential =
                GoogleAccountCredential.usingAudience(
                        context, "server:client_id:" + Constants.SERVER_CLIENT_ID);
        credential.setSelectedAccountName(googleSignInAccount.getEmail());
        Log.d(TAG, "credential account name " + credential.getSelectedAccountName());

        Fido2RequestHandler.Builder builder =
                new Fido2RequestHandler.Builder(
                        AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), credential)
                        .setGoogleClientRequestInitializer(
                                new GoogleClientRequestInitializer() {
                                    @Override
                                    public void initialize(AbstractGoogleClientRequest<?> abstractGoogleClientRequest)
                                            throws IOException {
                                        abstractGoogleClientRequest.setDisableGZipContent(true);
                                    }
                                });
        fido2Service = builder.build();
    }
}