// Copyright (C) 2012 The Android Open Source Project
//
// 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.gerrit.pgm.init;

import static java.util.Comparator.comparing;

import com.google.gerrit.common.PluginData;
import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugins.JarPluginProvider;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

@Singleton
public class InitPlugins implements InitStep {
  public static final String PLUGIN_DIR = "WEB-INF/plugins/";
  public static final String JAR = ".jar";

  public static List<PluginData> listPlugins(
      SitePaths site, PluginsDistribution pluginsDistribution) throws IOException {
    return listPlugins(site, false, pluginsDistribution);
  }

  public static List<PluginData> listPluginsAndRemoveTempFiles(
      SitePaths site, PluginsDistribution pluginsDistribution) throws IOException {
    return listPlugins(site, true, pluginsDistribution);
  }

  private static List<PluginData> listPlugins(
      final SitePaths site,
      final boolean deleteTempPluginFile,
      PluginsDistribution pluginsDistribution)
      throws IOException {
    final List<PluginData> result = new ArrayList<>();
    pluginsDistribution.foreach(
        (pluginName, in) -> {
          Path tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
          String pluginVersion = getVersion(tmpPlugin);
          if (deleteTempPluginFile) {
            Files.delete(tmpPlugin);
          }
          result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
        });
    result.sort(comparing(p -> p.name));
    return result;
  }

  private final ConsoleUI ui;
  private final SitePaths site;
  private final InitFlags initFlags;
  private final InitPluginStepsLoader pluginLoader;
  private final PluginsDistribution pluginsDistribution;

  private Injector postRunInjector;

  @Inject
  InitPlugins(
      final ConsoleUI ui,
      final SitePaths site,
      InitFlags initFlags,
      InitPluginStepsLoader pluginLoader,
      PluginsDistribution pluginsDistribution) {
    this.ui = ui;
    this.site = site;
    this.initFlags = initFlags;
    this.pluginLoader = pluginLoader;
    this.pluginsDistribution = pluginsDistribution;
  }

  @Override
  public void run() throws Exception {
    ui.header("Plugins");

    installPlugins();
    initPlugins();
  }

  @Override
  public void postRun() throws Exception {
    postInitPlugins();
  }

  @Inject(optional = true)
  void setPostRunInjector(Injector injector) {
    postRunInjector = injector;
  }

  private void installPlugins() throws IOException {
    ui.message("Installing plugins.\n");
    List<PluginData> plugins = listPlugins(site, pluginsDistribution);
    for (PluginData plugin : plugins) {
      String pluginName = plugin.name;
      try {
        final Path tmpPlugin = plugin.pluginPath;
        Path p = site.plugins_dir.resolve(plugin.name + ".jar");
        boolean upgrade = Files.exists(p);

        if (!(initFlags.installPlugins.contains(pluginName)
            || initFlags.installAllPlugins
            || ui.yesno(upgrade, "Install plugin %s version %s", pluginName, plugin.version))) {
          Files.deleteIfExists(tmpPlugin);
          continue;
        }

        if (upgrade) {
          final String installedPluginVersion = getVersion(p);
          if (!ui.yesno(
              upgrade,
              "%s %s is already installed, overwrite it",
              plugin.name,
              installedPluginVersion)) {
            Files.deleteIfExists(tmpPlugin);
            continue;
          }
          try {
            Files.delete(p);
          } catch (IOException e) {
            throw new IOException(
                "Failed to delete plugin " + pluginName + ": " + p.toAbsolutePath(), e);
          }
        }
        try {
          Files.move(tmpPlugin, p);
          if (upgrade) {
            // or update that is not an upgrade
            ui.message("Updated %s to %s\n", plugin.name, plugin.version);
          } else {
            ui.message("Installed %s %s\n", plugin.name, plugin.version);
          }
        } catch (IOException e) {
          throw new IOException(
              "Failed to install plugin "
                  + pluginName
                  + ": "
                  + tmpPlugin.toAbsolutePath()
                  + " -> "
                  + p.toAbsolutePath(),
              e);
        }
      } finally {
        Files.deleteIfExists(plugin.pluginPath);
      }
    }
    if (plugins.isEmpty()) {
      ui.message("No plugins found to install.\n");
    }
  }

  private void initPlugins() throws Exception {
    ui.message("Initializing plugins.\n");
    Collection<InitStep> initSteps = pluginLoader.getInitSteps();
    if (initSteps.isEmpty()) {
      ui.message("No plugins found with init steps.\n");
    } else {
      for (InitStep initStep : initSteps) {
        initStep.run();
      }
    }
  }

  private void postInitPlugins() throws Exception {
    for (InitStep initStep : pluginLoader.getInitSteps()) {
      postRunInjector.injectMembers(initStep);
      initStep.postRun();
    }
  }

  private static String getVersion(Path plugin) throws IOException {
    try (JarFile jarFile = new JarFile(plugin.toFile())) {
      Manifest manifest = jarFile.getManifest();
      Attributes main = manifest.getMainAttributes();
      return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
    }
  }
}
