// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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.api.ads.adwords.awalerting;

import com.google.api.ads.adwords.awalerting.processor.AlertProcessor;
import com.google.api.ads.adwords.awalerting.util.DynamicPropertyPlaceholderConfigurer;
import com.google.api.ads.adwords.awalerting.util.JaxWsProxySelector;
import com.google.common.base.Joiner;
import com.google.common.io.Files;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;

import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ProxySelector;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 * Main class that executes the alerts processing logic.
 *
 * <p>This class holds a Spring application context that manages the creation of all the beans
 * needed.
 *
 * <p>Credentials and properties are pulled from the ~/aw-report-alerting-sample.properties file
 * or -file <file> provided.
 *
 * <p>See README for more info.
 */
public class AwAlerting {
  private static final Logger LOGGER = LoggerFactory.getLogger(AwAlerting.class);
  private static final String SEPARATOR = System.getProperty("line.separator");

  /**
   * The Spring application context used to get all the beans.
   */
  private static ApplicationContext appCtx;

  /**
   * Main method.
   *
   * @param args the command line arguments
   */
  public static void main(String args[]) {
    // Set up proxy.
    JaxWsProxySelector ps = new JaxWsProxySelector(ProxySelector.getDefault());
    ProxySelector.setDefault(ps);

    Options options = createCommandLineOptions();
    try {
      CommandLineParser parser = new BasicParser();
      CommandLine cmdLine = parser.parse(options, args);

      // Print full help and quit
      if (cmdLine.hasOption("help")) {
        printHelpMessage(options);
        printSamplePropertiesFile();
        System.exit(0);
      }

      if (!cmdLine.hasOption("file")) {
        LOGGER.error("Missing required option: 'file'");
        System.exit(1);
      }

      processAlerts(cmdLine);
    } catch (ParseException e) {
      LOGGER.error("Error parsing the values for the command line options.", e);
      System.exit(1);
    } catch (AlertConfigLoadException e) {
      LOGGER.error("Error laoding alerts configuration.", e);
      System.exit(1);
    } catch (AlertProcessingException e) {
      LOGGER.error("Error processing alerts.", e);
      System.exit(1);
    }
  }

  /**
   * Main logic for creating processor and processing alerts according to configuration.
   *
   * @param cmdLine the command line arguments
   */
  private static void processAlerts(CommandLine cmdLine)
      throws AlertConfigLoadException, AlertProcessingException {
    String propertiesPath = cmdLine.getOptionValue("file");
    JsonObject alertsConfig = getAlertsConfig(propertiesPath);

    LOGGER.debug("Creating ReportProcessor bean...");
    AlertProcessor processor = createAlertProcessor();
    LOGGER.debug("... success.");

    LOGGER.info("*** Retrieving account IDs ***");
    Set<Long> clientCustomerIdsSet = null;
    if (cmdLine.hasOption("accountIdsFile")) {
      String accountsFileName = cmdLine.getOptionValue("accountIdsFile");
      clientCustomerIdsSet = getAccountsFromFile(accountsFileName);
      LOGGER.info("Accounts loaded from file: {}.", accountsFileName);
    }
    // If no "accountIdsFile" option, it will pass "clientCustomerIdsSet" as null and later
    // load all accounts under the manager account.

    processor.generateAlerts(clientCustomerIdsSet, alertsConfig);
  }

  /**
   * Load JSON configuration file specified in the properties file. First try to load the JSON
   * configuration file from the same folder as the properties file; if it does not exist, try to
   * load it from the default location.
   *
   * @param propertiesPath the path to the properties file
   * @return JSON configuration loaded from the json file
   * @throws AlertConfigLoadException error reading properties / json file
   */
  private static JsonObject getAlertsConfig(String propertiesPath) throws AlertConfigLoadException {
    LOGGER.info("Using properties file: {}", propertiesPath);

    Resource propertiesResource = new ClassPathResource(propertiesPath);
    if (!propertiesResource.exists()) {
      propertiesResource = new FileSystemResource(propertiesPath);
    }
    
    JsonObject alertsConfig = null;
    try {
      Properties properties = initApplicationContextAndProperties(propertiesResource);

      // Load alerts config from the same folder as the properties file
      String alertsConfigFilename = properties.getProperty("aw.alerting.alerts");
      String propertiesFolder = propertiesResource.getFile().getParent();
      File alertsConfigFile = new File(propertiesFolder, alertsConfigFilename);

      // If it does not exist, try the default resource folder according to maven structure.
      if (!alertsConfigFile.exists()) {
        String alertsConfigFilepath = "src/main/resources/" + alertsConfigFilename;
        alertsConfigFile = new File(alertsConfigFilepath);
      }

      LOGGER.debug("Loading alerts config file from {}", alertsConfigFile.getAbsolutePath());
      JsonParser jsonParser = new JsonParser();
      alertsConfig = jsonParser.parse(new FileReader(alertsConfigFile)).getAsJsonObject();
      LOGGER.debug("Done.");
    } catch (IOException e) {
      throw new AlertConfigLoadException("Error loading alerts config at " + propertiesPath, e);
    } catch (JsonParseException e) {
      throw new AlertConfigLoadException("Error parsing config file at " + propertiesPath, e);
    }

    return alertsConfig;
  }

  /**
   * Read the account ids from the stream.
   *
   * @param stream the stream to be read
   * @return a set of account ids in the file
   */
  protected static Set<Long> getAccountsFromStream(InputStream stream)
      throws AlertConfigLoadException {
    Set<Long> clientCustomerIdsSet = new LinkedHashSet<Long>();

    try {
      BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
      String line;
      while ((line = reader.readLine()) != null) {
        if (!line.startsWith("#")) {
          String clientCustomerIdStr = line.trim().replaceAll("-", "");
          Long clientCustomerId = Long.valueOf(clientCustomerIdStr);
          clientCustomerIdsSet.add(clientCustomerId);
        }
      }
      reader.close();
    } catch (IOException e) {
      throw new AlertConfigLoadException("Error reading accounts from stream.", e);
    }

    // Write the client customer IDs into debug log.
    LOGGER.debug("Client customer IDs specified from input:{}{}",
          SEPARATOR, Joiner.on(SEPARATOR).join(clientCustomerIdsSet));

    return clientCustomerIdsSet;
  }
  
  /**
   * Reads the account ids from specified file path.
   *
   * @param filepath the file path to be read from
   * @return a set of account ids in the file
   */
  private static Set<Long> getAccountsFromFile(String filepath)
      throws AlertConfigLoadException {
    InputStream stream = null;
    try {
      stream = new FileInputStream(filepath);
    } catch (FileNotFoundException e) {
      throw new AlertConfigLoadException("Error reading accounts from file path: " + filepath, e);
    }
    
    return getAccountsFromStream(stream);
  }

  /**
   * Creates the {@link AlertProcessor} autowiring all the dependencies.
   *
   * @return the {@code AlertProcessor} with all the dependencies properly injected
   */
  private static AlertProcessor createAlertProcessor() {
    return appCtx.getBean(AlertProcessor.class);
  }

  /**
   * Creates the command line options.
   *
   * @return the {@link Options}
   */
  private static Options createCommandLineOptions() {
    Options options = new Options();

    OptionBuilder.withArgName("help");
    OptionBuilder.hasArg(false);
    OptionBuilder.withDescription("print this message");
    OptionBuilder.isRequired(false);
    options.addOption(OptionBuilder.create("help"));

    OptionBuilder.withArgName("file");
    OptionBuilder.hasArg(true);
    OptionBuilder.withDescription("aw-report-alerting-sample.properties file.");
    OptionBuilder.isRequired(false);
    options.addOption(OptionBuilder.create("file"));

    OptionBuilder.withArgName("accountIdsFile");
    OptionBuilder.hasArg(true);
    OptionBuilder.withDescription(
        "Consider ONLY the account IDs specified on the file to run the report");
    OptionBuilder.isRequired(false);
    options.addOption(OptionBuilder.create("accountIdsFile"));

    OptionBuilder.withArgName("debug");
    OptionBuilder.hasArg(false);
    OptionBuilder.withDescription("Will display all the debug information. "
        + "If the option 'verbose' is activated, "
        + "all the information will be displayed on the console as well");
    OptionBuilder.isRequired(false);
    options.addOption(OptionBuilder.create("debug"));

    return options;
  }

  /**
   * Prints the help message.
   *
   * @param options the options available for the user
   */
  private static void printHelpMessage(Options options) {
    // automatically generate the help statement
    HelpFormatter formatter = new HelpFormatter();
    formatter.setWidth(120);
    formatter.printHelp(
        "\n  java -Xmx1G -jar aw-alerting.jar -file <file>", "\nArguments:", options, "");
    System.out.println();
  }

  /**
   * Prints the sample properties file on the default output.
   */
  private static void printSamplePropertiesFile() throws AlertConfigLoadException {
    System.out.println("File: aw-report-alerting-sample.properties example");
    ClassPathResource sampleFile = new ClassPathResource("aw-alerting-sample.properties");

    try {
      List<String> lines =
          Files.asCharSource(sampleFile.getFile(), Charset.defaultCharset()).readLines();
      for (String line : lines) {
        System.out.println(line);
      }
    } catch (IOException e) {
      throw new AlertConfigLoadException("Error reading sample properties file.", e);
    }
  }

  /**
   * Initialize the application context, adding the properties configuration file depending on the
   * specified path.
   *
   * @param propertiesResource the properties resource
   * @return the resource loaded from the properties file
   * @throws IOException error opening the properties file
   */
  private static Properties initApplicationContextAndProperties(Resource propertiesResource)
      throws IOException {
    LOGGER.trace("Innitializing Spring application context.");
    DynamicPropertyPlaceholderConfigurer.setDynamicResource(propertiesResource);
    Properties properties = PropertiesLoaderUtils.loadProperties(propertiesResource);

    // Selecting the XMLs to choose the Spring Beans to load.
    List<String> listOfClassPathXml = new ArrayList<String>();
    listOfClassPathXml.add("classpath:aw-alerting-processor-beans.xml");

    appCtx = new ClassPathXmlApplicationContext(
        listOfClassPathXml.toArray(new String[listOfClassPathXml.size()]));

    return properties;
  }
}