package com.avaje.ebeaninternal.server.autotune.service;

import com.avaje.ebean.config.AutoTuneConfig;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.api.SpiQuery;
import com.avaje.ebeaninternal.server.autotune.AutoTuneCollection;
import com.avaje.ebeaninternal.server.autotune.AutoTuneService;
import com.avaje.ebeaninternal.server.autotune.model.Autotune;
import com.avaje.ebeaninternal.server.autotune.model.Origin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;

/**
 * Implementation of the AutoTuneService which is comprised of profiling and query tuning.
 */
public class DefaultAutoTuneService implements AutoTuneService {

  private static final Logger logger = LoggerFactory.getLogger(DefaultAutoTuneService.class);

  private final SpiEbeanServer server;

  private final long defaultGarbageCollectionWait;

  private final boolean skipGarbageCollectionOnShutdown;

  private final boolean skipProfileReportingOnShutdown;

  private final BaseQueryTuner queryTuner;

  private final ProfileManager profileManager;

  private final boolean profiling;

  private final boolean queryTuning;

  private final String tuningFile;

  private final String profilingFile;

  private final String serverName;

  private final int profilingUpdateFrequency;

  private long runtimeChangeCount;

  public DefaultAutoTuneService(SpiEbeanServer server, ServerConfig serverConfig) {

    AutoTuneConfig config = serverConfig.getAutoTuneConfig();

    this.server = server;
    this.queryTuning = config.isQueryTuning();
    this.profiling = config.isProfiling();
    this.tuningFile = config.getQueryTuningFile();
    this.profilingFile = config.getProfilingFile();
    this.profilingUpdateFrequency = config.getProfilingUpdateFrequency();
    this.serverName = server.getName();
    this.profileManager = new ProfileManager(config, server);
    this.queryTuner = new BaseQueryTuner(config, server, profileManager);
    this.skipGarbageCollectionOnShutdown = config.isSkipGarbageCollectionOnShutdown();
    this.skipProfileReportingOnShutdown = config.isSkipProfileReportingOnShutdown();
    this.defaultGarbageCollectionWait = (long) config.getGarbageCollectionWait();
  }

  /**
   * Load the query tuning information from it's data store.
   */
  @Override
  public void startup() {

    if (queryTuning) {
      loadTuningFile();
      if (isRuntimeTuningUpdates()) {
        // periodically gather and update query tuning
        server.getBackgroundExecutor().executePeriodically(new ProfilingUpdate(), profilingUpdateFrequency, TimeUnit.SECONDS);
      }
    }
  }


  /**
   * Return true if the tuning should update periodically at runtime.
   */
  private boolean isRuntimeTuningUpdates() {
    return profilingUpdateFrequency > 0;
  }

  private class ProfilingUpdate implements Runnable {

    @Override
    public void run() {
      runtimeTuningUpdate();
    }
  }

  /**
   * Load tuning information from an existing tuning file.
   */
  private void loadTuningFile() {
    File file = new File(tuningFile);
    if (file.exists()) {
      loadAutoTuneProfiling(AutoTuneXmlReader.read(file));
    } else {
      // look for autotune as a resource
      InputStream stream = getClass().getResourceAsStream("/" + tuningFile);
      if (stream != null) {
        loadAutoTuneProfiling(AutoTuneXmlReader.read(stream));
      } else {
        logger.warn("AutoTune file {} not found - no initial automatic query tuning", tuningFile);
      }
    }
  }

  private void loadAutoTuneProfiling(Autotune profiling) {
    logger.info("AutoTune loading {} tuning entries", profiling.getOrigin().size());
    for (Origin origin : profiling.getOrigin()) {
      queryTuner.put(origin);
    }
  }

  /**
   * Collect profiling, check for new/diff to existing tuning and apply changes.
   */
  private void runtimeTuningUpdate() {

    synchronized (this) {
      try {
        long start = System.currentTimeMillis();

        AutoTuneCollection profiling = profileManager.profilingCollection(false);

        AutoTuneDiffCollection event = new AutoTuneDiffCollection(profiling, queryTuner, true);
        event.process();
        if (event.isEmpty()) {
          long exeMillis = System.currentTimeMillis() - start;
          logger.debug("No query tuning updates for server:{} executionMillis:{}", serverName, exeMillis);

        } else {
          // report the query tuning changes that have been made
          runtimeChangeCount += event.getChangeCount();
          event.writeFile(profilingFile + "-" + serverName + "-update");
          long exeMillis = System.currentTimeMillis() - start;
          logger.info("query tuning updates - new:{} diff:{} for server:{} executionMillis:{}", event.getNewCount(), event.getDiffCount(), serverName, exeMillis);
        }
      } catch (Throwable e) {
        logger.error("Error collecting or applying automatic query tuning", e);
      }
    }
  }

  private void saveProfilingOnShutdown(boolean reset) {

    synchronized (this) {
      if (isRuntimeTuningUpdates()) {
        runtimeTuningUpdate();
        outputAllTuning();

      } else {

        AutoTuneCollection profiling = profileManager.profilingCollection(reset);

        AutoTuneDiffCollection event = new AutoTuneDiffCollection(profiling, queryTuner, false);
        event.process();
        if (event.isEmpty()) {
          logger.info("No new or diff entries for profiling server:{}", serverName);

        } else {
          event.writeFile(profilingFile + "-" + serverName);
          logger.info("writing new:{} diff:{} profiling entries for server:{}", event.getNewCount(), event.getDiffCount(), serverName);
        }
      }
    }
  }

  /**
   * Output all the query tuning (the "all" file).
   * <p>
   * This is the originally loaded tuning plus any tuning changes picked up and applied at runtime.
   * </p>
   * <p>
   * This "all" file can be used as the next "ebean-autotune.xml" file.
   * </p>
   */
  private void outputAllTuning() {

    if (runtimeChangeCount == 0) {
      logger.info("no runtime query tuning changes for server:{}", serverName);

    } else {
      AutoTuneAllCollection event = new AutoTuneAllCollection(queryTuner);
      int size = event.size();
      File existingTuning = new File(tuningFile);
      if (existingTuning.exists()) {
        // rename the existing autotune.xml file (appending 'now')
        if (!existingTuning.renameTo(new File(tuningFile+"."+AutoTuneXmlWriter.now()))) {
          logger.warn("Failed to rename autotune file [{}]", tuningFile);
        }
      }

      event.writeFile(tuningFile, false);
      logger.info("query tuning detected [{}] changes, writing all [{}] tuning entries for server:{}", runtimeChangeCount, size, serverName);
    }
  }

  /**
   * Shutdown the listener.
   * <p>
   * We should try to collect the usage statistics by calling a System.gc().
   * This is necessary for use with short lived applications where garbage
   * collection may not otherwise occur at all.
   * </p>
   */
  @Override
  public void shutdown() {
    if (profiling) {
      if (!skipGarbageCollectionOnShutdown && !skipProfileReportingOnShutdown) {
        // trigger GC to update profiling information on recently executed queries
        collectProfiling(-1);
      }
      if (!skipProfileReportingOnShutdown) {
        saveProfilingOnShutdown(false);
      }
    }
  }

  /**
   * Output the profiling.
   * <p>
   * When profiling updates are applied to tuning at runtime this reports all tuning and profiling combined.
   * When profiling is not applied at runtime then this reports the diff report with new and diff entries relative
   * to the existing tuning.
   * </p>
   */
  public void reportProfiling() {
    saveProfilingOnShutdown(false);
  }

  /**
   * Ask for a System.gc() so that we gather node usage information.
   * <p>
   * Really only want to do this sparingly but useful just prior to shutdown
   * for short run application where garbage collection may otherwise not
   * occur at all.
   * </p>
   * <p>
   * waitMillis will do a thread sleep to give the garbage collection a little
   * time to do its thing assuming we are shutting down the VM.
   * </p>
   * <p>
   * If waitMillis is -1 then the defaultGarbageCollectionWait is used which
   * defaults to 100 milliseconds.
   * </p>
   */
  @Override
  public void collectProfiling() {
    collectProfiling(-1);
  }

  public void collectProfiling(long waitMillis) {
    System.gc();
    try {
      if (waitMillis < 0) {
        waitMillis = defaultGarbageCollectionWait;
      }
      Thread.sleep(waitMillis);
    } catch (InterruptedException e) {
      // restore the interrupted status
      Thread.currentThread().interrupt();
      logger.warn("Error while sleeping after System.gc() request.", e);
    }
  }

  /**
   * Auto tune the query and enable profiling.
   */
  @Override
  public boolean tuneQuery(SpiQuery<?> query) {
    return queryTuner.tuneQuery(query);
  }

}
