/*
 * Copyright 2016 Google LLC.
 *
 * 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.tools.appengine.operations;

import com.google.cloud.tools.appengine.AppEngineException;
import com.google.cloud.tools.appengine.configuration.DeployConfiguration;
import com.google.cloud.tools.appengine.configuration.DeployProjectConfigurationConfiguration;
import com.google.cloud.tools.appengine.operations.cloudsdk.CloudSdkNotFoundException;
import com.google.cloud.tools.appengine.operations.cloudsdk.CloudSdkOutOfDateException;
import com.google.cloud.tools.appengine.operations.cloudsdk.internal.args.GcloudArgs;
import com.google.cloud.tools.appengine.operations.cloudsdk.process.ProcessHandlerException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;

/** Deploy staged application and project configuration. */
public class Deployment {

  // optional gcloud modes
  private static final ImmutableList<String> GCLOUD_MODES = ImmutableList.of("alpha", "beta");

  private final GcloudRunner runner;

  Deployment(GcloudRunner runner) {
    this.runner = Preconditions.checkNotNull(runner);
  }

  /**
   * Deploys a project to App Engine.
   *
   * @throws CloudSdkNotFoundException when the Cloud SDK is not installed where expected
   * @throws CloudSdkOutOfDateException when the installed Cloud SDK is too old
   * @throws AppEngineException when there is an issue uploading project files to the cloud
   * @throws IllegalArgumentException when a local deployable referenced by the configuration isn't
   *     found
   */
  public void deploy(DeployConfiguration config) throws AppEngineException {
    Preconditions.checkNotNull(config);
    Preconditions.checkNotNull(config.getDeployables());
    Preconditions.checkArgument(config.getDeployables().size() > 0);
    Path workingDirectory = null;

    List<String> arguments = new ArrayList<>();
    String mode = processMode(config.getGcloudMode());
    if (mode != null) {
      arguments.add(mode);
    }
    arguments.add("app");
    arguments.add("deploy");

    // Unfortunately, 'gcloud app deploy' does not let you pass a staging directory as a deployable.
    // Instead, we have to run 'gcloud app deploy' from the staging directory to achieve this.
    // So, if we find that the only deployable in the list is a directory, we just run the command
    // from that directory without passing in any deployables to gcloud.
    if (config.getDeployables().size() == 1 && Files.isDirectory(config.getDeployables().get(0))) {
      workingDirectory = config.getDeployables().get(0);
    } else {
      for (Path deployable : config.getDeployables()) {
        if (!Files.exists(deployable)) {
          throw new IllegalArgumentException("Deployable " + deployable + " does not exist.");
        }
        arguments.add(deployable.toString());
      }
    }

    arguments.addAll(GcloudArgs.get("bucket", config.getBucket()));
    arguments.addAll(GcloudArgs.get("image-url", config.getImageUrl()));
    arguments.addAll(GcloudArgs.get("promote", config.getPromote()));
    arguments.addAll(GcloudArgs.get("server", config.getServer()));
    arguments.addAll(GcloudArgs.get("stop-previous-version", config.getStopPreviousVersion()));
    arguments.addAll(GcloudArgs.get("version", config.getVersion()));
    arguments.addAll(GcloudArgs.get("project", config.getProjectId()));

    try {
      runner.run(arguments, workingDirectory);
    } catch (ProcessHandlerException | IOException ex) {
      throw new AppEngineException(ex);
    }
  }

  /** Deploy cron.yaml to App Engine. */
  public void deployCron(DeployProjectConfigurationConfiguration config) throws AppEngineException {
    deployConfig("cron.yaml", config);
  }

  /** Deploy dos.yaml to App Engine. */
  public void deployDos(DeployProjectConfigurationConfiguration config) throws AppEngineException {
    deployConfig("dos.yaml", config);
  }

  /** Deploy dispatch.yaml to App Engine. */
  public void deployDispatch(DeployProjectConfigurationConfiguration config)
      throws AppEngineException {
    deployConfig("dispatch.yaml", config);
  }

  /** Deploy index.yaml to App Engine. */
  public void deployIndex(DeployProjectConfigurationConfiguration config)
      throws AppEngineException {
    deployConfig("index.yaml", config);
  }

  /** Deploy queue.yaml to App Engine. */
  public void deployQueue(DeployProjectConfigurationConfiguration config)
      throws AppEngineException {
    deployConfig("queue.yaml", config);
  }

  /**
   * Common configuration deployment function.
   *
   * @param filename Yaml file that we want to deploy (cron.yaml, dos.yaml, etc)
   * @param configuration Deployment configuration
   */
  @VisibleForTesting
  void deployConfig(String filename, DeployProjectConfigurationConfiguration configuration)
      throws AppEngineException {
    Preconditions.checkNotNull(configuration);
    Preconditions.checkNotNull(configuration.getAppEngineDirectory());

    Path deployable = configuration.getAppEngineDirectory().resolve(filename);
    Preconditions.checkArgument(
        Files.isRegularFile(deployable), deployable.toString() + " does not exist.");

    List<String> arguments = new ArrayList<>();
    arguments.add("app");
    arguments.add("deploy");
    arguments.add(deployable.toAbsolutePath().toString());
    arguments.addAll(GcloudArgs.get("server", configuration.getServer()));
    arguments.addAll(GcloudArgs.get("project", configuration.getProjectId()));

    try {
      runner.run(arguments, null);
    } catch (ProcessHandlerException | IOException ex) {
      throw new AppEngineException(ex);
    }
  }

  @VisibleForTesting
  @Nullable
  String processMode(@Nullable String modeFromConfig) throws AppEngineException {
    if (modeFromConfig == null) {
      return null;
    }
    String trimmedModeFromConfig = modeFromConfig.trim();
    Optional<String> matchingMode =
        GCLOUD_MODES.stream().filter(trimmedModeFromConfig::equalsIgnoreCase).findFirst();
    return matchingMode.orElseThrow(
        () -> new AppEngineException("Invalid gcloud mode: " + modeFromConfig));
  }
}