// Copyright 2017 Google Inc.
//
// 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.google.webauthn.gaedemo.servlets;

import java.io.IOException;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.webauthn.gaedemo.objects.AttestationConveyancePreference;
import com.google.webauthn.gaedemo.objects.AuthenticationExtensionsClientInputs;
import com.google.webauthn.gaedemo.objects.AuthenticatorAttachment;
import com.google.webauthn.gaedemo.objects.AuthenticatorSelectionCriteria;
import com.google.webauthn.gaedemo.objects.PublicKeyCredentialCreationOptions;
import com.google.webauthn.gaedemo.objects.PublicKeyCredentialDescriptor;
import com.google.webauthn.gaedemo.objects.PublicKeyCredentialType;
import com.google.webauthn.gaedemo.objects.UserVerificationRequirement;
import com.google.webauthn.gaedemo.storage.CableKeyPair;
import com.google.webauthn.gaedemo.storage.Credential;
import com.google.webauthn.gaedemo.storage.SessionData;

public class BeginMakeCredential extends HttpServlet {

  private static final long serialVersionUID = 1L;
  private final UserService userService = UserServiceFactory.getUserService();

  public BeginMakeCredential() {}

  @Override
  protected void doGet(HttpServletRequest a, HttpServletResponse b)
      throws ServletException, IOException {
    doPost(a, b);
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    User user = userService.getCurrentUser();
    // String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host");
    String rpId = Iterables.get(Splitter.on(':').split(request.getHeader("Host")), 0);
    String rpName = getServletContext().getInitParameter("name");
    rpName = (rpName == null ? "" : rpName);

    PublicKeyCredentialCreationOptions options =
        new PublicKeyCredentialCreationOptions(user.getNickname(), user.getEmail(), rpId, rpName);

    String hasAdvanced = request.getParameter("advanced");
    if (hasAdvanced.equals("true")) {
      parseAdvancedOptions(request.getParameter("advancedOptions"), options);
    }

    SessionData session = new SessionData(options.challenge, rpId);

    session.save(userService.getCurrentUser().getEmail());
    JsonObject sessionJson = session.getJsonObject();
    JsonObject optionsJson = options.getJsonObject();
    optionsJson.add("session", sessionJson);

    AuthenticationExtensionsClientInputs extensions = new AuthenticationExtensionsClientInputs();
    try {
      KeyPair cableKeyPair = extensions.addCableRegistrationData();
      // Store the KeyPair in storage
      CableKeyPair storedKeyPair = new CableKeyPair(cableKeyPair);
      storedKeyPair.save(session.getId());
    } catch (Exception e) {}

    optionsJson.add("extensions", extensions.getRegistrationExtensions());

    response.setContentType("application/json");
    response.getWriter().println(optionsJson.toString());
  }

  private void parseAdvancedOptions(String jsonString, PublicKeyCredentialCreationOptions options) {
    JsonElement jsonElement = new JsonParser().parse(jsonString);
    JsonObject jsonObject = jsonElement.getAsJsonObject();
    Set<Map.Entry<String, JsonElement>> entries = jsonObject.entrySet();

    boolean rk = false;
    boolean excludeCredentials = false;
    UserVerificationRequirement uv = null;
    AuthenticatorAttachment attachment = null;
    for (Map.Entry<String, JsonElement> entry : entries) {
      if (entry.getKey().equals("requireResidentKey")) {
        rk = entry.getValue().getAsBoolean();
      } else if (entry.getKey().equals("excludeCredentials")) {
        excludeCredentials = entry.getValue().getAsBoolean();

        if (excludeCredentials) {
          List<PublicKeyCredentialDescriptor> credentials = new ArrayList<>();
          String currentUser = userService.getCurrentUser().getEmail();
          List<Credential> savedCreds = Credential.load(currentUser);
          for (Credential c : savedCreds) {
            credentials.add(convertCredentialToCredentialDescriptor(c));
          }
          options.setExcludeCredentials(credentials);
        }
      } else if (entry.getKey().equals("userVerification")) {
        uv = UserVerificationRequirement.decode(entry.getValue().getAsString());
      } else if (entry.getKey().equals("authenticatorAttachment")) {
        attachment = AuthenticatorAttachment.decode(entry.getValue().getAsString());
      } else if (entry.getKey().equals("attestationConveyancePreference")) {
        AttestationConveyancePreference conveyance =
            AttestationConveyancePreference.decode(entry.getValue().getAsString());
        options.setAttestationConveyancePreference(conveyance);
      }
    }

    options.setCriteria(new AuthenticatorSelectionCriteria(attachment, rk, uv));
  }

  private PublicKeyCredentialDescriptor convertCredentialToCredentialDescriptor(Credential c) {
    PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY;
    byte[] id = c.getCredential().getRawId();

    return new PublicKeyCredentialDescriptor(type, id, null);
  }

}