/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.server.upgrade;

import java.io.File;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.ControllerModule;
import org.apache.ambari.server.orm.DBAccessor;
import org.apache.ambari.server.utils.VersionUtils;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.persist.PersistService;

public class SchemaUpgradeHelper {
  private static final Logger LOG = LoggerFactory.getLogger
    (SchemaUpgradeHelper.class);

  private Set<UpgradeCatalog> allUpgradeCatalogs;
  private PersistService persistService;
  private DBAccessor dbAccessor;
  private Configuration configuration;

  @Inject
  public SchemaUpgradeHelper(Set<UpgradeCatalog> allUpgradeCatalogs,
                             PersistService persistService,
                             DBAccessor dbAccessor,
                             Configuration configuration) {
    this.allUpgradeCatalogs = allUpgradeCatalogs;
    this.persistService = persistService;
    this.dbAccessor = dbAccessor;
    this.configuration = configuration;
  }

  public void startPersistenceService() {
    persistService.start();
  }

  public void stopPersistenceService() {
    persistService.stop();
  }

  public Set<UpgradeCatalog> getAllUpgradeCatalogs() {
    return allUpgradeCatalogs;
  }

  public String readSourceVersion() {

    ResultSet resultSet = null;
    try {
      resultSet = dbAccessor.executeSelect("SELECT " + dbAccessor.quoteObjectName("metainfo_value") +
          " from metainfo WHERE " + dbAccessor.quoteObjectName("metainfo_key") + "='version'");
      if (resultSet.next()) {
        return resultSet.getString(1);
      } else {
        //not found, assume oldest version
        //doesn't matter as there single upgrade catalog for 1.2.0 - 1.5.0 and 1.4.4 - 1.5.0 upgrades
        return "1.2.0";
      }
    } catch (SQLException e) {
      throw new RuntimeException("Unable to read database version", e);
    }finally {
      if (resultSet != null) {
        try {
          resultSet.close();
        } catch (SQLException e) {
          throw new RuntimeException("Cannot close result set");
        }
      }
    }

  }

  /**
   * Read server version file
   * @return
   */
  protected String getAmbariServerVersion() {
    String versionFilePath = configuration.getServerVersionFilePath();
    try {
      return FileUtils.readFileToString(new File(versionFilePath)).trim();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return null;
  }

  /**
   * Return a set Upgrade catalogs to be applied to upgrade from
   * @sourceVersion to @targetVersion
   *
   * @param sourceVersion
   * @param targetVersion
   * @return
   * @throws org.apache.ambari.server.AmbariException
   */
  protected List<UpgradeCatalog> getUpgradePath(String sourceVersion,
                                                       String targetVersion) throws AmbariException {

    List<UpgradeCatalog> upgradeCatalogs = new ArrayList<UpgradeCatalog>();
    List<UpgradeCatalog> candidateCatalogs = new ArrayList<UpgradeCatalog>(allUpgradeCatalogs);

    Collections.sort(candidateCatalogs, new AbstractUpgradeCatalog.VersionComparator());

    for (UpgradeCatalog upgradeCatalog : candidateCatalogs) {
      if (sourceVersion == null || VersionUtils.compareVersions(sourceVersion,
        upgradeCatalog.getTargetVersion(), 3) < 0) {
        // catalog version is newer than source
        if (VersionUtils.compareVersions(upgradeCatalog.getTargetVersion(),
          targetVersion, 3) <= 0) {
          // catalog version is older or equal to target
          upgradeCatalogs.add(upgradeCatalog);
        }
      }
    }

    LOG.info("Upgrade path: " + upgradeCatalogs);

    return upgradeCatalogs;
  }

  /**
   * Extension of main controller module
   */
  public static class UpgradeHelperModule extends ControllerModule {

    public UpgradeHelperModule() throws Exception {
    }

    public UpgradeHelperModule(Properties properties) throws Exception {
      super(properties);
    }

    @Override
    protected void configure() {
      super.configure();
      // Add binding to each newly created catalog
      Multibinder<UpgradeCatalog> catalogBinder =
        Multibinder.newSetBinder(binder(), UpgradeCatalog.class);
      catalogBinder.addBinding().to(UpgradeCatalog150.class);
      catalogBinder.addBinding().to(UpgradeCatalog151.class);
      catalogBinder.addBinding().to(UpgradeCatalog160.class);
      catalogBinder.addBinding().to(UpgradeCatalog161.class);
      catalogBinder.addBinding().to(UpgradeCatalog170.class);
      catalogBinder.addBinding().to(UpgradeCatalog200.class);
    }
  }

  public void executeUpgrade(List<UpgradeCatalog> upgradeCatalogs) throws AmbariException {
    LOG.info("Executing DDL upgrade...");

    if (upgradeCatalogs != null && !upgradeCatalogs.isEmpty()) {
      for (UpgradeCatalog upgradeCatalog : upgradeCatalogs) {
        try {
          upgradeCatalog.upgradeSchema();
        } catch (Exception e) {
          LOG.error("Upgrade failed. ", e);
          throw new AmbariException(e.getMessage(), e);
        }
      }
    }
  }

  public void executeDMLUpdates(List<UpgradeCatalog> upgradeCatalogs) throws AmbariException {
    LOG.info("Executing DML changes.");

    if (upgradeCatalogs != null && !upgradeCatalogs.isEmpty()) {
      for (UpgradeCatalog upgradeCatalog : upgradeCatalogs) {
        try {
          upgradeCatalog.upgradeData();
        } catch (Exception e) {
          LOG.error("Upgrade failed. ", e);
          throw new AmbariException(e.getMessage(), e);
        }
      }
    }
  }

  public void resetUIState() throws AmbariException {
    LOG.info("Resetting UI state.");
    try {
      dbAccessor.updateTable("key_value_store", dbAccessor.quoteObjectName("value"),
          "{\"clusterState\":\"CLUSTER_STARTED_5\"}",
          "where " + dbAccessor.quoteObjectName("key") + "='CLUSTER_CURRENT_STATUS'");
    } catch (SQLException e) {
      throw new AmbariException("Unable to reset UI state", e);
    }
  }

  /**
   * Upgrade Ambari DB schema to the target version passed in as the only
   * argument.
   * @param args args[0] = target version to upgrade to.
   */
  public static void main(String[] args) throws Exception {
    try {
      Injector injector = Guice.createInjector(new UpgradeHelperModule());
      SchemaUpgradeHelper schemaUpgradeHelper = injector.getInstance(SchemaUpgradeHelper.class);

      String targetVersion = schemaUpgradeHelper.getAmbariServerVersion();
      LOG.info("Upgrading schema to target version = " + targetVersion);

      UpgradeCatalog targetUpgradeCatalog = AbstractUpgradeCatalog
        .getUpgradeCatalog(targetVersion);

      LOG.debug("Target upgrade catalog. " + targetUpgradeCatalog);

      // Read source version from DB
      String sourceVersion = schemaUpgradeHelper.readSourceVersion();
      LOG.info("Upgrading schema from source version = " + sourceVersion);

      List<UpgradeCatalog> upgradeCatalogs =
        schemaUpgradeHelper.getUpgradePath(sourceVersion, targetVersion);

      schemaUpgradeHelper.executeUpgrade(upgradeCatalogs);

      schemaUpgradeHelper.startPersistenceService();

      schemaUpgradeHelper.executeDMLUpdates(upgradeCatalogs);

      schemaUpgradeHelper.resetUIState();

      LOG.info("Upgrade successful.");

      schemaUpgradeHelper.stopPersistenceService();
    } catch (Throwable e) {
      if (e instanceof AmbariException) {
        LOG.error("Exception occured during upgrade, failed", e);
        throw (AmbariException)e;
      }else{
        LOG.error("Unexpected error, upgrade failed", e);
        throw new Exception("Unexpected error, upgrade failed", e);
      }
    }
  }
}
