/*
 * Copyright 2013 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.google.cloud.solutions.sampleapps.orchestration.orchestrator.server;

import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Collection;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Class used for creating GCE instances.
 */
public class GceInstanceCreator {

  private static final Logger logger = Logger.getLogger(GceInstanceCreator.class.getName());

  /**
   * Creates one or more new instances.
   *
   * @param numExistingInstances the number of existing instances.
   * @param numInstancesToCreate the number of new instances to create.
   * @param existingInstanceNames the map from IPs to instance names
   * @throws OrchestratorException if create new instances failed.
   */
  public boolean createNewInstances(int numExistingInstances, int numInstancesToCreate,
      long maxTimeout, Collection<String> existingInstanceNames) throws OrchestratorException {
    long timeout = System.currentTimeMillis() + maxTimeout;
    int curSuffix = 0;
    for (int i = 0; i < numInstancesToCreate; i++) {
      try {
        Map<String, String> configProperties =
            ConfigProperties.getInstance().getGceConfigProperties();
        String prefix = configProperties.get("instancePrefix");
        curSuffix = findNextAvailableSuffix(curSuffix++, existingInstanceNames, prefix);
        String instanceName = prefix + Integer.toString(curSuffix);
        String diskName = configProperties.get("diskName") + instanceName;
        String projectApiKey = configProperties.get("projectApiKey");
        String accessToken = GceApiUtils.getAccessTokenForComputeScope();

        // 1. Create new disk.
        createDisk(diskName, accessToken, projectApiKey, configProperties);

        // 2. Poll to wait until the disk is ready or until we time out.
        boolean diskCreated = false;
        String url = GceApiUtils.composeDiskApiUrl(
            ConfigProperties.urlPrefixWithProjectAndZone, diskName, projectApiKey);

        while (!diskCreated && System.currentTimeMillis() < timeout) {
          if (checkDiskOrInstance(accessToken, url)) {
            diskCreated = true;
            logger.info("Disk is ready.");
          } else {
            logger.info("Disk is not ready. Sleeping for two seconds ...");
            // Wait for 2 seconds.
            Thread.sleep(2000);
          }
        }
        if (!diskCreated) {
          logger.warning("Timed out. Giving up.");
          return false;
        }

        boolean instanceCreated = false;
        try {
          // 3. Create instance
          createInstance(instanceName, diskName, projectApiKey, accessToken, configProperties);

          // 4. Poll to wait until the instance is ready or until we time out.
          url = GceApiUtils.composeInstanceApiUrl(
              ConfigProperties.urlPrefixWithProjectAndZone, instanceName, projectApiKey);
          while (!instanceCreated && System.currentTimeMillis() < timeout) {
            if (checkDiskOrInstance(accessToken, url)) {
              instanceCreated = true;
              logger.info("Instance is ready.");
            } else {
              logger.info("Instance is not ready. Sleeping for two seconds ...");
              // Wait for 2 seconds.
              Thread.sleep(2000);
            }
          }
        } catch (OrchestratorException e) {
          // We have to catch this exception so we can delete the disk if it has already been
          // created.
          logger.warning("Orchestrator Exception caught: " + e.getMessage());
        }
        if (!instanceCreated) {

          // Before returning, delete the disk that has already been created.
          String deleteUrl = GceApiUtils.composeDiskApiUrl(
              ConfigProperties.urlPrefixWithProjectAndZone, diskName, projectApiKey);
          GceApiUtils.deleteDisk(diskName, accessToken, deleteUrl);

          logger.warning("Timed out. Giving up.");
          return false;
        }
        numExistingInstances++;
      } catch (MalformedURLException e) {
        throw new OrchestratorException(e);
      } catch (IOException e) {
        throw new OrchestratorException(e);
      } catch (InterruptedException e) {
        throw new OrchestratorException(e);
      }
    }
    // All instances have been created within the timeout time frame.
    return true;
  }

  /**
   * @param seed the current seed, where the method will begin looking for the next available suffix
   * @param existingInstanceNames a full list of existing instance names before this orchestration
   *        decision
   * @param prefix the prefix for all instance names (set in the config.xml file)
   * @return the next available suffix
   * @throws OrchestratorException if no new suffix can be found
   */
  private int findNextAvailableSuffix(
      int seed, Collection<String> existingInstanceNames, String prefix)
          throws OrchestratorException {
    while (seed < Integer.MAX_VALUE) {
      String proposedNewInstanceName = prefix + Integer.toString(seed);
      if (!existingInstanceNames.contains(proposedNewInstanceName)) {
        return seed;
      }
      seed++;
    }
    throw new OrchestratorException(
        "Failed to find any available suffixes. Cannot create more instances.");
  }

  /**
   * Creates a new instance.
   *
   * @param instanceName the name of the instance to create.
   * @param bootDiskName the name of the disk to create the instance with.
   * @param projectApiKey the project API key.
   * @param accessToken the access token.
   * @param configProperties the configuration properties.
   * @throws MalformedURLException
   * @throws IOException
   * @throws OrchestratorException if the REST API call failed to create instance.
   */
  private void createInstance(String instanceName, String bootDiskName, String projectApiKey,
      String accessToken, Map<String, String> configProperties)
      throws MalformedURLException, IOException, OrchestratorException {
    String url = GceApiUtils.composeInstanceApiUrl(
        ConfigProperties.urlPrefixWithProjectAndZone, projectApiKey);
    String payload = createPayload_instance(instanceName, bootDiskName, configProperties);
    logger.info(
        "Calling " + url + " to create instance " + instanceName + "with payload " + payload);
    HTTPResponse httpResponse =
        GceApiUtils.makeHttpRequest(accessToken, url, payload, HTTPMethod.POST);
    int responseCode = httpResponse.getResponseCode();
    if (!(responseCode == 200 || responseCode == 204)) {
      throw new OrchestratorException("Failed to create GCE instance. " + instanceName
          + ". Response code " + responseCode + " Reason: "
          + new String(httpResponse.getContent()));
    }
  }

  /**
   * Creates a new disk.
   *
   * @param diskName the name of the disk to create.
   * @param accessToken the access token.
   * @param configProperties the configuration properties.
   * @throws MalformedURLException
   * @throws IOException
   * @throws OrchestratorException if the REST API call failed to create disk.
   */
  private void createDisk(String diskName, String accessToken, String projectApiKey,
      Map<String, String> configProperties)
      throws MalformedURLException, IOException, OrchestratorException {
    String url =
        GceApiUtils.composeDiskApiUrl(ConfigProperties.urlPrefixWithProjectAndZone, projectApiKey);
    String payload = createPayload_disk(diskName, configProperties);
    HTTPResponse httpResponse =
        GceApiUtils.makeHttpRequest(accessToken, url, payload, HTTPMethod.POST);
    int responseCode = httpResponse.getResponseCode();
    if (!(responseCode == 200 || responseCode == 204)) {
      throw new OrchestratorException("Failed to create Disk " + diskName + ". Response code "
          + responseCode + " Reason: " + new String(httpResponse.getContent()));
    }
  }

  /**
   * Checks whether the disk or instance is available.
   *
   * @param accessToken the access token.
   * @param url the URL to check whether the disk/instance has been created.
   * @return true if the disk/instance is available, false otherwise.
   * @throws MalformedURLException
   * @throws IOException
   */
  private boolean checkDiskOrInstance(String accessToken, String url)
      throws MalformedURLException, IOException {
    HTTPResponse httpResponse = GceApiUtils.makeHttpRequest(accessToken, url, "", HTTPMethod.GET);
    int responseCode = httpResponse.getResponseCode();
    if (!(responseCode == 200 || responseCode == 204)) {
      logger.fine("Disk/instance not ready. Response code " + responseCode + " Reason: "
          + new String(httpResponse.getContent()));
      return false;
    }
    // Check if the disk/instance is in status "READY".
    String contentStr = new String(httpResponse.getContent());
    JsonParser parser = new JsonParser();
    JsonObject o = (JsonObject) parser.parse(contentStr);
    String status = o.get("status").getAsString();
    if (!status.equals("READY") && !status.equals("RUNNING")) {
      return false;
    }
    return true;
  }

  /**
   * Makes the payload for creating a new disk.
   *
   * @param diskName the name of the disk.
   * @return the payload for the POST request to create a new disk.
   */
  private String createPayload_disk(String diskName, Map<String, String> configProperties) {
    JsonObject json = new JsonObject();
    json.addProperty("kind", "compute#disk");
    json.addProperty("name", diskName);
    json.addProperty("zone", ConfigProperties.urlPrefixWithProjectAndZone);
    json.addProperty("description", "Persistent boot disk.");
    json.addProperty("sourceSnapshot", ConfigProperties.urlPrefixWithProject + "/global/snapshots/"
        + configProperties.get("snapshotName"));
    String payload = json.toString();
    return payload;
  }

  /**
   * Makes the payload for creating an instance.
   *
   * @param instanceName the name of the instance.
   * @return the payload for the POST request to create a new instance.
   */
  String createPayload_instance(
      String instanceName, String bootDiskName, Map<String, String> configProperties) {
    JsonObject json = new JsonObject();
    json.addProperty("kind", "compute#instance");
    json.addProperty("name", instanceName);
    json.addProperty("machineType", ConfigProperties.urlPrefixWithProjectAndZone + "/machineTypes/"
        + configProperties.get("machineType"));

    JsonObject disksElem = new JsonObject();
    disksElem.addProperty("kind", "compute#attachedDisk");
    disksElem.addProperty("boot", true);
    disksElem.addProperty("type", "PERSISTENT");
    disksElem.addProperty("mode", "READ_WRITE");
    disksElem.addProperty("deviceName", bootDiskName);
    disksElem.addProperty("zone", ConfigProperties.urlPrefixWithProjectAndZone);
    disksElem.addProperty(
        "source", ConfigProperties.urlPrefixWithProjectAndZone + "/disks/" + bootDiskName);

    JsonArray jsonAr = new JsonArray();
    jsonAr.add(disksElem);
    json.add("disks", jsonAr);

    JsonObject networkInterfacesObj = new JsonObject();
    networkInterfacesObj.addProperty("kind", "compute#instanceNetworkInterface");
    networkInterfacesObj.addProperty(
        "network", ConfigProperties.urlPrefixWithProject + "/global/networks/default");

    JsonObject accessConfigsObj = new JsonObject();
    accessConfigsObj.addProperty("name", "External NAT");
    accessConfigsObj.addProperty("type", "ONE_TO_ONE_NAT");
    JsonArray accessConfigsAr = new JsonArray();
    accessConfigsAr.add(accessConfigsObj);
    networkInterfacesObj.add("accessConfigs", accessConfigsAr);

    JsonArray networkInterfacesAr = new JsonArray();
    networkInterfacesAr.add(networkInterfacesObj);
    json.add("networkInterfaces", networkInterfacesAr);

    JsonObject serviceAccountsObj = new JsonObject();
    serviceAccountsObj.addProperty("kind", "compute#serviceAccount");
    serviceAccountsObj.addProperty("email", "default");
    JsonArray scopesAr = new JsonArray();
    scopesAr.add(new JsonPrimitive("https://www.googleapis.com/auth/userinfo.email"));
    scopesAr.add(new JsonPrimitive("https://www.googleapis.com/auth/compute"));
    scopesAr.add(new JsonPrimitive("https://www.googleapis.com/auth/devstorage.full_control"));
    serviceAccountsObj.add("scopes", scopesAr);
    JsonArray serviceAccountsAr = new JsonArray();
    serviceAccountsAr.add(serviceAccountsObj);
    json.add("serviceAccounts", serviceAccountsAr);

    JsonObject metadataObj = new JsonObject();

    JsonArray mdItemsAr = new JsonArray();
    JsonObject mdItemsObj = new JsonObject();
    mdItemsObj.addProperty("key", "startup-script-url");
    mdItemsObj.addProperty("value", configProperties.get("startupScript"));
    mdItemsAr.add(mdItemsObj);
    metadataObj.add("items", mdItemsAr);
    json.add("metadata", metadataObj);
    String payload = json.toString();
    return payload;
  }
}