/*
 * Copyright 2017 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.managedcloudsdk;

import static com.google.cloud.tools.managedcloudsdk.OsInfo.Name.WINDOWS;

import com.google.cloud.tools.appengine.operations.cloudsdk.serialization.CloudSdkComponent;
import com.google.cloud.tools.appengine.operations.cloudsdk.serialization.CloudSdkComponent.State;
import com.google.cloud.tools.managedcloudsdk.command.CommandCaller;
import com.google.cloud.tools.managedcloudsdk.command.CommandExecutionException;
import com.google.cloud.tools.managedcloudsdk.command.CommandExitException;
import com.google.cloud.tools.managedcloudsdk.components.SdkComponent;
import com.google.cloud.tools.managedcloudsdk.components.SdkComponentInstaller;
import com.google.cloud.tools.managedcloudsdk.components.SdkUpdater;
import com.google.cloud.tools.managedcloudsdk.install.SdkInstaller;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

/** A manager for installing, configuring and updating the Cloud SDK. */
public class ManagedCloudSdk {

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

  private final Version version;
  private final Path managedSdkDirectory;
  private final OsInfo osInfo;

  /** Instantiated with {@link ManagedCloudSdk#newManagedSdk}. */
  ManagedCloudSdk(Version version, Path managedSdkDirectory, OsInfo osInfo) {
    this.version = version;
    this.managedSdkDirectory = managedSdkDirectory;
    this.osInfo = osInfo;
  }

  public Path getSdkHome() {
    return managedSdkDirectory.resolve(version.getVersion()).resolve("google-cloud-sdk");
  }

  /** Returns a path to gcloud executable (operating system specific). */
  public Path getGcloudPath() {
    return getSdkHome()
        .resolve("bin")
        .resolve(osInfo.name().equals(WINDOWS) ? "gcloud.cmd" : "gcloud");
  }

  /** Simple check to verify Cloud SDK installed by verifying the existence of gcloud. */
  public boolean isInstalled()
      throws ManagedSdkVerificationException, ManagedSdkVersionMismatchException {
    if (getSdkHome() == null) {
      return false;
    }
    if (!Files.isDirectory(getSdkHome())) {
      return false;
    }
    if (!Files.isRegularFile(getGcloudPath())) {
      return false;
    }
    // Verify the versions match up for fixed version installs
    if (version != Version.LATEST) {
      try {
        String versionFileContents =
            new String(Files.readAllBytes(getSdkHome().resolve("VERSION")), StandardCharsets.UTF_8)
                .trim();
        if (!versionFileContents.equals(version.getVersion())) {
          throw new ManagedSdkVersionMismatchException(
              "Installed sdk version: "
                  + versionFileContents
                  + " does not match expected version: "
                  + version.getVersion()
                  + ".");
        }
      } catch (IOException ex) {
        throw new ManagedSdkVerificationException(ex);
      }
    }
    return true;
  }

  /**
   * Query gcloud to see if component is installed. Uses gcloud's '--local-state-only' to avoid
   * network accesses.
   */
  public boolean hasComponent(SdkComponent component) throws ManagedSdkVerificationException {
    if (!Files.isRegularFile(getGcloudPath())) {
      return false;
    }

    List<String> listComponentCommand =
        Arrays.asList(
            getGcloudPath().toString(),
            "components",
            "list",
            "--only-local-state",
            "--format=json",
            "--filter=id:" + component);

    try {
      String result = CommandCaller.newCaller().call(listComponentCommand, null, null);
      List<CloudSdkComponent> components = CloudSdkComponent.fromJsonList(result);
      if (components.size() > 1) {
        // not a unique component id
        throw new ManagedSdkVerificationException("Invalid component " + component);
      }
      return !components.isEmpty();
    } catch (CommandExecutionException | InterruptedException | CommandExitException ex) {
      throw new ManagedSdkVerificationException(ex);
    }
  }

  /** Query gcloud to see if SDK is up to date. Gcloud makes a call to the server to check this. */
  public boolean isUpToDate() throws ManagedSdkVerificationException {
    if (!Files.isRegularFile(getGcloudPath())) {
      return false;
    }

    if (version != Version.LATEST) {
      return true;
    }

    List<String> updateAvailableCommand =
        Arrays.asList(
            getGcloudPath().toString(),
            "components",
            "list",
            "--format=json",
            "--filter=state.name:Update Available");

    try {
      String result = CommandCaller.newCaller().call(updateAvailableCommand, null, null);
      for (CloudSdkComponent component : CloudSdkComponent.fromJsonList(result)) {
        State state = component.getState();
        if (state != null) {
          if ("Update Available".equals(state.getName())) {
            return false;
          }
        }
      }
      return true;
    } catch (CommandExecutionException | InterruptedException | CommandExitException ex) {
      throw new ManagedSdkVerificationException(ex);
    }
  }

  // TODO : fix passthrough for useragent and client side usage reporting
  public SdkInstaller newInstaller() {
    String userAgentString = "google-cloud-tools-java";
    return SdkInstaller.newInstaller(managedSdkDirectory, version, osInfo, userAgentString, false);
  }

  public SdkComponentInstaller newComponentInstaller() {
    return SdkComponentInstaller.newComponentInstaller(osInfo.name(), getGcloudPath());
  }

  /**
   * For "LATEST" version SDKs, the client tooling must keep the SDK up-to-date manually, check with
   * {@link #isUpToDate()} before using, returns a new updater if sdk is "LATEST", it will throw a
   * {@link UnsupportedOperationException} if SDK is a fixed version (fixed versions should never be
   * udpated).
   */
  public SdkUpdater newUpdater() {
    if (version != Version.LATEST) {
      throw new UnsupportedOperationException("Cannot update a fixed version SDK.");
    }
    return SdkUpdater.newUpdater(osInfo.name(), getGcloudPath());
  }

  /** Get a new {@link ManagedCloudSdk} instance for @{link Version} specified. */
  public static ManagedCloudSdk newManagedSdk(Version version) throws UnsupportedOsException {
    OsInfo osInfo = OsInfo.getSystemOsInfo();
    return new ManagedCloudSdk(
        version,
        getOsSpecificManagedSdkHome(osInfo.name(), System.getProperties(), System.getenv()),
        osInfo);
  }

  /** Convenience method to obtain a new LATEST {@link ManagedCloudSdk} instance. */
  public static ManagedCloudSdk newManagedSdk() throws UnsupportedOsException {
    return newManagedSdk(Version.LATEST);
  }

  @VisibleForTesting
  static Path getOsSpecificManagedSdkHome(
      OsInfo.Name osName, Properties systemProperties, Map<String, String> environment) {
    Path userHome = Paths.get(systemProperties.getProperty("user.home"));
    Path cloudSdkPartialPath = Paths.get("google-cloud-tools-java", "managed-cloud-sdk");
    Path xdgPath = userHome.resolve(".cache").resolve(cloudSdkPartialPath);

    switch (osName) {
      case WINDOWS:
        // Shorter path to mitigate the length limit issue on Windows
        Path shortCloudSdkPartialPath = Paths.get("google", "ct4j-cloud-sdk");
        Path windowsXdgPath = userHome.resolve(".cache").resolve(shortCloudSdkPartialPath);

        String localAppDataEnv = environment.get("LOCALAPPDATA");
        if (localAppDataEnv == null || localAppDataEnv.trim().isEmpty()) {
          logger.warning("LOCALAPPDATA environment is invalid or missing");
          return windowsXdgPath;
        }
        Path localAppData = Paths.get(localAppDataEnv);
        if (!Files.exists(localAppData)) {
          logger.warning(localAppData.toString() + " does not exist");
          return windowsXdgPath;
        }
        return localAppData.resolve(shortCloudSdkPartialPath);

      case MAC:
        Path applicationSupport = userHome.resolve("Library").resolve("Application Support");
        if (!Files.exists(applicationSupport)) {
          logger.warning(applicationSupport.toString() + " does not exist");
          return xdgPath;
        }
        return applicationSupport.resolve(cloudSdkPartialPath);

      case LINUX:
        return xdgPath;

      default:
        // we can't actually get here unless we modify the enum OsInfo.Name
        throw new RuntimeException("OsName is not valid : " + osName);
    }
  }
}