/*
 * Copyright 2016 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.example.appengine.firetactoe;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.appengine.http.UrlFetchTransport;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.appengine.api.appidentity.AppIdentityService;
import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
import com.google.common.io.BaseEncoding;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Utility functions for communicating with the realtime communication channel using Firebase. In
 * this app, we use Firebase as a communication bus to push the state of the board to all clients -
 * that is, players of the game. This class contains the methods used to communicate with Firebase.
 */
public class FirebaseChannel {

  private static final String FIREBASE_SNIPPET_PATH = "WEB-INF/view/firebase_config.jspf";
  static InputStream firebaseConfigStream = null;
  private static final Collection FIREBASE_SCOPES =
      Arrays.asList(
          "https://www.googleapis.com/auth/firebase.database",
          "https://www.googleapis.com/auth/userinfo.email");
  private static final String IDENTITY_ENDPOINT =
      "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";

  private String firebaseDbUrl;
  private GoogleCredential credential;
  // Keep this a package-private member variable, so that it can be mocked for unit tests
  HttpTransport httpTransport;

  private static FirebaseChannel instance;

  /**
   * FirebaseChannel is a singleton, since it's just utility functions. The class derives auth
   * information when first instantiated.
   */
  public static FirebaseChannel getInstance() {
    if (instance == null) {
      instance = new FirebaseChannel();
    }
    return instance;
  }

  /**
   * Construct the singleton, with derived auth information. The Firebase database url is derived
   * from the snippet that we provide to the client code, to guarantee that the client and the
   * server are communicating with the same Firebase database. The auth credentials we'll use to
   * communicate with Firebase is derived from App Engine's default credentials, and given
   * Firebase's OAuth scopes.
   */
  private FirebaseChannel() {
    try {
      // This variables exist primarily so it can be stubbed out in unit tests.
      if (null == firebaseConfigStream) {
        firebaseConfigStream = new FileInputStream(FIREBASE_SNIPPET_PATH);
      }

      String firebaseSnippet =
          CharStreams.toString(new InputStreamReader(firebaseConfigStream, StandardCharsets.UTF_8));
      firebaseDbUrl = parseFirebaseUrl(firebaseSnippet);

      credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
      httpTransport = UrlFetchTransport.getDefaultInstance();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Parses out the Firebase database url from the client-side code snippet. The code snippet is a
   * piece of javascript that defines an object with the key 'databaseURL'. So look for that key,
   * then parse out its quote-surrounded value.
   */
  private static String parseFirebaseUrl(String firebaseSnippet) {
    int idx = firebaseSnippet.indexOf("databaseURL");
    if (-1 == idx) {
      throw new RuntimeException(
          "Please copy your Firebase web snippet into " + FIREBASE_SNIPPET_PATH);
    }
    idx = firebaseSnippet.indexOf(':', idx);
    int openQuote = firebaseSnippet.indexOf('"', idx);
    int closeQuote = firebaseSnippet.indexOf('"', openQuote + 1);
    return firebaseSnippet.substring(openQuote + 1, closeQuote);
  }

  /**
   * sendFirebaseMessage.
   * @param channelKey .
   * @param game .
   * @throws IOException .
   */
  public void sendFirebaseMessage(String channelKey, Game game) throws IOException {
    // Make requests auth'ed using Application Default Credentials
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
    GenericUrl url =
        new GenericUrl(String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
    HttpResponse response = null;

    try {
      if (null == game) {
        response = requestFactory.buildDeleteRequest(url).execute();
      } else {
        String gameJson = new Gson().toJson(game);
        response =
            requestFactory
                .buildPatchRequest(
                    url, new ByteArrayContent("application/json", gameJson.getBytes()))
                .execute();
      }

      if (response.getStatusCode() != 200) {
        throw new RuntimeException(
            "Error code while updating Firebase: " + response.getStatusCode());
      }

    } finally {
      if (null != response) {
        response.disconnect();
      }
    }
  }

  /**
   * Create a secure JWT token for the given userId.
   */
  public String createFirebaseToken(Game game, String userId) {
    final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService();
    final BaseEncoding base64 = BaseEncoding.base64();

    String header = base64.encode("{\"typ\":\"JWT\",\"alg\":\"RS256\"}".getBytes());

    // Construct the claim
    String channelKey = game.getChannelKey(userId);
    String clientEmail = appIdentity.getServiceAccountName();
    long epochTime = System.currentTimeMillis() / 1000;
    long expire = epochTime + 60 * 60; // an hour from now

    Map<String, Object> claims = new HashMap<String, Object>();
    claims.put("iss", clientEmail);
    claims.put("sub", clientEmail);
    claims.put("aud", IDENTITY_ENDPOINT);
    claims.put("uid", channelKey);
    claims.put("iat", epochTime);
    claims.put("exp", expire);

    String payload = base64.encode(new Gson().toJson(claims).getBytes());
    String toSign = String.format("%s.%s", header, payload);
    AppIdentityService.SigningResult result = appIdentity.signForApp(toSign.getBytes());
    return String.format("%s.%s", toSign, base64.encode(result.getSignature()));
  }

  // The following methods are to illustrate making various calls to Firebase from App Engine
  // Standard

  /**
   * firebasePut.
   * @param path .
   * @param object .
   * @return .
   * @throws IOException .
   */
  public HttpResponse firebasePut(String path, Object object) throws IOException {
    // Make requests auth'ed using Application Default Credentials
    Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

    String json = new Gson().toJson(object);
    GenericUrl url = new GenericUrl(path);

    return requestFactory
        .buildPutRequest(url, new ByteArrayContent("application/json", json.getBytes()))
        .execute();
  }

  /**
   * firebasePatch.
   * @param path .
   * @param object .
   * @return .
   * @throws IOException .
   */
  public HttpResponse firebasePatch(String path, Object object) throws IOException {
    // Make requests auth'ed using Application Default Credentials
    Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

    String json = new Gson().toJson(object);
    GenericUrl url = new GenericUrl(path);

    return requestFactory
        .buildPatchRequest(url, new ByteArrayContent("application/json", json.getBytes()))
        .execute();
  }

  /**
   * firebasePost.
   * @param path .
   * @param object .
   * @return .
   * @throws IOException .
   */
  public HttpResponse firebasePost(String path, Object object) throws IOException {
    // Make requests auth'ed using Application Default Credentials
    Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

    String json = new Gson().toJson(object);
    GenericUrl url = new GenericUrl(path);

    return requestFactory
        .buildPostRequest(url, new ByteArrayContent("application/json", json.getBytes()))
        .execute();
  }

  /**
   * firebaseGet.
   * @param path .
   * @return .
   * @throws IOException .
   */
  public HttpResponse firebaseGet(String path) throws IOException {
    // Make requests auth'ed using Application Default Credentials
    Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

    GenericUrl url = new GenericUrl(path);

    return requestFactory.buildGetRequest(url).execute();
  }

  /**
   * firebaseDelete.
   * @param path .
   * @return .
   * @throws IOException .
   */
  public HttpResponse firebaseDelete(String path) throws IOException {
    // Make requests auth'ed using Application Default Credentials
    Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

    GenericUrl url = new GenericUrl(path);

    return requestFactory.buildDeleteRequest(url).execute();
  }
}
