/*
 * 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.gobblin.runtime.template;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import com.google.api.client.util.Charsets;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigResolveOptions;

import org.apache.gobblin.configuration.ConfigurationKeys;
import org.apache.gobblin.runtime.api.JobTemplate;
import org.apache.gobblin.runtime.api.SpecNotFoundException;
import org.apache.gobblin.runtime.job_catalog.PackagedTemplatesJobCatalogDecorator;
import org.apache.gobblin.util.ConfigUtils;
import org.apache.gobblin.util.PathUtils;
import org.apache.gobblin.util.PullFileLoader;

import lombok.Data;


/**
 * A simple script to convert old style pull files into *.conf files using templates.
 *
 * This script will load *.pull files and a template, and will attempt to create a *.conf file using that template
 * that resolves to the same job configuration as the pull file resolves. To do this, it compares all of the config
 * key-value pairs in the pull file against those in the template, and writes out only those key-value pairs that
 * are different from the template. It then writes the *.conf file to the output directory.
 *
 * The template can include a key {@link #DO_NOT_OVERRIDE_KEY} with an array of strings representing keys in the template
 * that should not appear in the output *.conf even if they resolve to different values.
 *
 * Note the template must be a resource-style template and it must be in the classpath.
 *
 * Usage:
 * PullFileToConfigConverter <pullFilesRootPath> <pullFilesToConvertGlob> resource:///<templatePath> <sysConfigPath> <outputDir>
 *
 */
@Data
public class PullFileToConfigConverter {

  public static final String DO_NOT_OVERRIDE_KEY = "pullFileToConfigConverter.doNotOverride";

  private static final Set<String> FILTER_KEYS = ImmutableSet.of("job.config.path", "jobconf.dir", "jobconf.fullyQualifiedPath");

  private final Path pullFileRootPath;
  private final Path fileGlobToConvert;
  private final Path templateURI;
  private final File sysConfigPath;
  private final Path outputPath;

  public static void main(String[] args) throws Exception {
    if (args.length != 5) {
      System.out.println("Usage: PullFileToConfigConverter <pullFilesRootPath> <pullFilesToConvertGlob> resource:///<templatePath> <sysConfigPath> <outputDir>");
      System.exit(1);
    }

    new PullFileToConfigConverter(new Path(args[0]), new Path(args[1]), new Path(args[2]), new File(args[3]), new Path(args[4]))
        .convert();
  }

  public void convert() throws IOException {
    Config baseConfig = ConfigFactory.parseString(DO_NOT_OVERRIDE_KEY + ": []");

    FileSystem pullFileFs = pullFileRootPath.getFileSystem(new Configuration());
    FileSystem outputFs = this.outputPath.getFileSystem(new Configuration());

    Config sysConfig = ConfigFactory.parseFile(this.sysConfigPath);

    PullFileLoader pullFileLoader = new PullFileLoader(this.pullFileRootPath, pullFileFs,
        PullFileLoader.DEFAULT_JAVA_PROPS_PULL_FILE_EXTENSIONS, PullFileLoader.DEFAULT_HOCON_PULL_FILE_EXTENSIONS);

    PackagedTemplatesJobCatalogDecorator catalog = new PackagedTemplatesJobCatalogDecorator();

    ConfigResolveOptions configResolveOptions = ConfigResolveOptions.defaults();
    configResolveOptions = configResolveOptions.setAllowUnresolved(true);

    ResourceBasedJobTemplate template;
    Config templateConfig;
    try {
      template = (ResourceBasedJobTemplate) catalog.getTemplate(templateURI.toUri());

      templateConfig = sysConfig.withFallback(template.getRawTemplateConfig()).withFallback(baseConfig).resolve(configResolveOptions);
    } catch (SpecNotFoundException|JobTemplate.TemplateException exc) {
      throw new IOException(exc);
    }

    Set<String> doNotOverride = templateConfig.hasPath(DO_NOT_OVERRIDE_KEY) ?
        Sets.newHashSet(templateConfig.getStringList(DO_NOT_OVERRIDE_KEY)) : Sets.<String>newHashSet();

    ConfigRenderOptions configRenderOptions = ConfigRenderOptions.defaults();
    configRenderOptions = configRenderOptions.setComments(false);
    configRenderOptions = configRenderOptions.setOriginComments(false);
    configRenderOptions = configRenderOptions.setFormatted(true);
    configRenderOptions = configRenderOptions.setJson(false);

    for (FileStatus pullFile : pullFileFs.globStatus(this.fileGlobToConvert)) {
      Config pullFileConfig = pullFileLoader.loadPullFile(pullFile.getPath(), ConfigFactory.empty(), true).resolve();
      Map<String, String> outputConfigMap = Maps.newHashMap();

      outputConfigMap.put(ConfigurationKeys.JOB_TEMPLATE_PATH, this.templateURI.toString());

      boolean somethingChanged;
      do {
        somethingChanged = false;

        Config currentOutputConfig = ConfigFactory.parseMap(outputConfigMap);
        Config currentResolvedConfig = currentOutputConfig.withFallback(templateConfig).resolve(configResolveOptions);

        for (Map.Entry<Object, Object> entry : ConfigUtils.configToProperties(pullFileConfig).entrySet()) {
          String key = (String) entry.getKey();
          String value = (String) entry.getValue();

          try {
            if ((!currentResolvedConfig.hasPath(key)) ||
                (!currentResolvedConfig.getString(key).equals(value) && !doNotOverride.contains(key))) {
              if (!FILTER_KEYS.contains(key)) {
                somethingChanged = true;
                outputConfigMap.put(key, value);
              }
            }
          } catch (ConfigException.NotResolved nre) {
            // path is unresolved in config, will try again next iteration
          }
        }

      } while (somethingChanged);

      try {
        Config outputConfig = ConfigFactory.parseMap(outputConfigMap);
        Config currentResolvedConfig = outputConfig.withFallback(templateConfig).resolve();

        String rendered = outputConfig.root().render(configRenderOptions);

        Path newPath = PathUtils.removeExtension(pullFile.getPath(),
            PullFileLoader.DEFAULT_JAVA_PROPS_PULL_FILE_EXTENSIONS.toArray(new String[]{}));
        newPath = PathUtils.addExtension(newPath, "conf");
        newPath = new Path(this.outputPath, newPath.getName());

        FSDataOutputStream os = outputFs.create(newPath);
        os.write(rendered.getBytes(Charsets.UTF_8));
        os.close();
      } catch (ConfigException.NotResolved nre) {
        throw new IOException("Not all configuration keys were resolved in pull file " + pullFile.getPath(), nre);
      }

    }
  }
}
