/*******************************************************************************
 *
 *   Copyright 2015 Walmart, Inc.
 *
 *   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.oneops.inductor;

import com.oneops.cms.domain.CmsWorkOrderSimpleBase;
import com.oneops.cms.simple.domain.CmsActionOrderSimple;
import com.oneops.cms.simple.domain.CmsWorkOrderSimple;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Config {

  private static Logger logger = Logger.getLogger(Config.class);

  @Value("${packer_home}")
  private String circuitDir;

  @Value("${amq.in_queue}")
  private String inQueue;

  @Value("${retry_count:3}")
  private int retryCount;

  // allows use of public or private ip
  @Value("${ip_attribute}")
  private String ipAttribute;

  @Value("${data_dir}")
  private String dataDir;

  @Value("${mgmt_domain}")
  private String mgmtDomain;

  @Value("${perf_collector_cert_location:unset}")
  private String perfCollectorCertLocation;

  @Value("${mgmt_url}")
  private String mgmtUrl;

  @Value("${mgmt_cert:unset}")
  private String mgmtCert;

  @Value("${min_free_space_mb:50}")
  private int minFreeSpaceMB;

  // For backwards compliance (using PPC.ignoreUnresolvablePlaceholders)
  // missing autowired values default to a String, Hence this dns value
  // to boolean inside init.
  @Value("${dns}")
  private String dnsEnabled;

  private boolean dnsDisabled = false;

  @Value("${dns_config_file}")
  private String dnsConfigFile;

  // Added to debug compute add - prevents compute::delete on
  // compute::add (install_base/remote) failure
  @Value("${debug_mode}")
  private String debugMode;

  // Enable jmx metrics
  @Value("${enable_jmx:true}")
  private boolean isJMXEnabled;

  // Enable auto shutdown
  @Value("${autoShutDown:false}")
  private boolean isAutoShutDown;

  @Value("${initial_user:unset}")
  private String initialUser;

  @Value("${local_max_consumers:10}")
  private int localMaxConsumers;

  // rsync timeout. Default value is 30 sec
  @Value("${rsync_timeout:30}")
  private int rsyncTimeout;

  /**
   * The list of clouds which are marked to be in stub mode. Inductor will mark those work-order and
   * action orders execution result as per <value>stubResultCode</value>
   */
  @Value("#{'${stub.clouds:}'.toLowerCase().split(',')}")
  private List<String> stubbedCloudsList;

  /**
   * The list of clouds, whose resources are already decommissioned or removed. Inductor will mark
   * those work-order (no action orders) execution result as success (0) regardless of the execution
   * outcome for the clouds listed in this (shutdown.clouds) inductor property. Spring EPL default
   * value is empty string.
   */
  @Value("#{'${shutdown.clouds:}'.toLowerCase().split(',')}")
  private List<String> clouds;

  /**
   * Default to fail; Config should be provided to make the resultCode '1' as failure.
   */
  @Value("${stubResultCode:1}")
  private int stubResultCode;

  /**
   * How long should inductor wait before returning a stubbed response ? Defaults to 5 seconds
   */
  @Value("${stub.responseTimeInSeconds:5}")
  private int stubResponseTimeInSeconds;

  @Value("${autoShutDownThreshold:99.99}")
  private double autoShutDownThreshold;

  /**
   * List of bom classes, whose process result status needs to keep intact. By default
   * <b>bom.Fqdn</b>,<b>bom.Lb</b> are added to this list as it doesn't have any openstack
   * hypervisor dependency.
   */
  @Value("#{'${shutdown.skipClasses:bom.Fqdn,bom.Lb}'.toLowerCase().split(',')}")
  private List<String> bomClasses;

  /**
   * List of rfc actions for which the result need to be processed. <b>DELETE</b>action is added by
   * default.
   */
  @Value("#{'${shutdown.rfcActions:DELETE}'.toLowerCase().split(',')}")
  private List<String> rfcActions;

  /**
   * Timeout  value for command execution for shutdown clouds. Default value is set as 10 sec.
   */
  @Value("${shutdown.cmdTimeout:10}")
  private long cmdTimeout;

  @Value("${chef_timeout:7200}")
  private long chefTimeout;

  /**
   * Additional env variables to be used for work-order exec. The value can be file location or a
   * string containing multiple ENV_NAME=VALUE entries. Entries are separated by newline (file) or
   * ',' (string). Right now this configuration is used only for local work-orders.
   */
  @Value("${env_vars:}")
  private String env;

  /**
   * Max number of reboots that can happen in a single work-order execution.
   */
  @Value("${reboot_limit:5}")
  private int rebootLimit;

  @Value("#{T(java.lang.Boolean).valueOf('${verify.mode}')}")
  private boolean verifyMode;

  @Value("#{'${verify.exclude.paths:Berksfile}'.split(',')}")
  private List<String> verifyExcludePaths;

  /**
   * Pass additional args for test-kitchen
   * LOG_LEVEL (debug, info, warn, error, fatal)
   * e.g.  --log-level=LOG_LEVEL
   *       --no-color
   */
  @Value("${verify.args:}")
  private String verifyArgs;

  /**
   * Env vars read from {@link #env}. This will get initialized in ${@link #init()}
   */
  private Map<String, String> envVars;

  private String publicKey = "";

  private String dnsKey = null;

  private String dnsSecret = null;

  private String ipAddr = null;

  private String mgmtCertContent = null;

  private String perfCollectorCertContent = null;

  /**
   * init - configuration / defaults
   */
  public void init() {
    // Read env vars.
    envVars = readEnvVars(env);

    // Null checks due to spring ignoreUnresolvablePlaceholders
    // not working on junit tests.
    if (mgmtDomain == null) {
      mgmtDomain = getMgmtDomainFromFile();
    }

    // Read mgmt certificate file content
    mgmtCertContent = readCertFile(mgmtCert);
    perfCollectorCertContent = readCertFile(perfCollectorCertLocation);

    // defaults for some backwards compliance
    if (circuitDir == null) {
      circuitDir = "/opt/oneops/inductor";
    }

    if (dataDir == null || dataDir.equals("${data_dir}")) {
      dataDir = "/opt/oneops/tmp";
    }
    if (ipAttribute == null || ipAttribute.equals("${ip_attribute}")) {
      ipAttribute = "public_ip";
    }

    if (dnsEnabled == null || dnsEnabled.equalsIgnoreCase("off") || dnsEnabled
        .equalsIgnoreCase("false")) {
      dnsDisabled = true;
    }
    if (dnsConfigFile == null || dnsConfigFile.equals("${dns_config_file}")) {
      dnsConfigFile = "/opt/oneops/inductor/global/dns.conf";
    }
    if (debugMode == null) {
      debugMode = "off";
    }
    // Sets the IP for logging.
    ipAddr = getInductorIPv4Addr();
    if (ipAddr == null) {
      ipAddr = "N/A";
    }

    getGlobalDnsConfFromFile();

    Map<String, String> env = System.getenv();
    String envPackerDir = env.get("PACKER_DIR");
    if (envPackerDir != null) {
      circuitDir = envPackerDir;
    }

    if (clouds == null) {
      clouds = new ArrayList<>();
    }

    // Remove the empty default
    clouds.remove("");
    if (!clouds.isEmpty()) {
      logger.info("*** " + this.toString());
    }
  }

  /**
   * get mgmt domain so computes know how to talk to daq
   */
  private String getMgmtDomainFromFile() {
    String outFile = "/opt/oneops/domain";
    String block = "";
    String thisLine;
    BufferedReader br;
    try {
      br = new BufferedReader(new FileReader(outFile));
      while ((thisLine = br.readLine()) != null) {
        block += thisLine;
      }
      br.close();
    } catch (IOException e) {
      logger.error("cannot read /opt/oneops/domain");
    }
    return block;
  }

  /**
   * Read the given cert file.
   *
   * @param certFile cert file path
   * @return string containing content of the file.
   */
  private String readCertFile(String certFile) {
    if (certFile == null || "unset".equals(certFile)) {
      return null;
    }

    String block = "";
    try (BufferedReader br = new BufferedReader(new FileReader(certFile))) {
      String line;
      while ((line = br.readLine()) != null) {
        block += line + "\r\n";
      }
    } catch (Exception e) {
      logger.error("Cannot read " + certFile, e);
      return null;
    }
    return block;
  }

  /**
   * get mgmt domain so computes know how to talk to daq
   */
  private void getGlobalDnsConfFromFile() {
    String thisLine;
    BufferedReader br;
    logger.info("using dns config: " + dnsConfigFile);
    try {
      br = new BufferedReader(new FileReader(dnsConfigFile));
      int count = 0;
      while ((thisLine = br.readLine()) != null) {
        if (count == 0) {
          dnsKey = thisLine.replaceAll("\n", "");
          logger.info("using " + InductorConstants.DEFAULT_DOMAIN
              + " key:" + dnsKey);
        } else if (count == 1) {
          dnsSecret = thisLine.replaceAll("\n", "");
          logger.info("using " + InductorConstants.DEFAULT_DOMAIN
              + " secret:" + dnsSecret);
        }
        count++;
      }
    } catch (FileNotFoundException e) {
      logger.info("not a public inductor - missing: " + dnsConfigFile);
    } catch (IOException e) {
      logger.info("cannot read " + dnsConfigFile);
    }
  }

  /**
   * Retruns the inductor IP address (IPV4 address). If there are multiple NICs/IfAddresses, it
   * selects the first one. Openstack VMs normally has only one network interface (eth0).
   *
   * @return IPV4 address of inductor with interface name. Returns <code>null</code> if it couldn't
   * find anything.
   */
  private String getInductorIPv4Addr() {
    try {
      Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
      while (nics.hasMoreElements()) {
        NetworkInterface nic = nics.nextElement();
        if (nic.isUp() && !nic.isLoopback()) {
          Enumeration<InetAddress> addrs = nic.getInetAddresses();
          while (addrs.hasMoreElements()) {
            InetAddress add = addrs.nextElement();
            // Print only IPV4 address
            if (add instanceof Inet4Address && !add.isLoopbackAddress()) {
              // Log the first one.
              String ip = add.getHostAddress() + " (" + nic.getDisplayName() + ")";
              logger.info("Inductor IP : " + ip);
              return ip;
            }
          }
        }
      }
    } catch (Exception e) {
      logger.warn("Error getting inductor IP address", e);
      // Skip any errors
    }
    return null;
  }

  /**
   * Helper method to read inductor ${@link #env} and returns an env vars map.
   *
   * @param env env can be a file location or a string containing multiple ENV_NAME=VALUE entries.
   * Entries are separated by newline (file) or ',' (string).
   * @return env var map.
   */
  private Map<String, String> readEnvVars(String env) {
    Path path = Paths.get(env);
    List<String> kvList;
    if (path.toFile().exists()) {
      try {
        kvList = Files.readAllLines(path);
      } catch (IOException ioe) {
        logger.warn("Error reading env var file: " + path, ioe);
        kvList = Collections.emptyList();
      }
    } else {
      kvList = Arrays.asList(env.trim().split(","));
    }
    return kvList.stream()
        .map(s -> s.split("="))
        .filter(p -> p.length == 2)
        .collect(Collectors.toMap(p -> p[0].trim(), p -> p[1].trim()));
  }

  /**
   * Checks if the cloud for given wo has been configured as shutdown
   *
   * @param bwo work order
   * @return <code>true</code> if the wo cloud has configured as shutdown, else return
   * <code>false</code>
   */
  public boolean hasCloudShutdownFor(CmsWorkOrderSimpleBase bwo) {
    if (!clouds.isEmpty()) {
      // Proceed only if it's not null
      if (bwo != null) {
        // Do it only for work orders
        if (bwo instanceof CmsWorkOrderSimple) {
          CmsWorkOrderSimple wo = CmsWorkOrderSimple.class.cast(bwo);
          // Do it only for configured rfc actions (DELETE by default)
          if (rfcActions.contains(wo.getRfcCi().getRfcAction().toLowerCase())) {
            String cloudName = wo.getCloud().getCiName();
            // Do it only for the shutdown clouds
            if (clouds.contains(cloudName.toLowerCase())) {
              String bomClass = wo.getRfcCi().getCiClassName();
              // Skip configured bom classes
              return !bomClasses.contains(bomClass.toLowerCase());
            }
          }

        }
      }

    }
    return false;
  }

  public boolean isCloudStubbed(CmsWorkOrderSimpleBase bwo) {
    boolean stubbedMode = false;
    String cloudName = StringUtils.EMPTY;
    if (!CollectionUtils.isEmpty(stubbedCloudsList)) {
      if (bwo != null) {
        cloudName = getCloud(bwo, cloudName);
        if (StringUtils.isEmpty(cloudName)) {
          stubbedMode = false;
        } else if (stubbedCloudsList.contains(cloudName.toLowerCase())) {
          stubbedMode = true;
        }
      }
    }
    if (stubbedMode) {
      logger.warn("Cloud :" + cloudName + " is running in stub mode.");
    }
    return stubbedMode;
  }

  private String getCloud(CmsWorkOrderSimpleBase bwo, String cloudName) {
    if (bwo instanceof CmsWorkOrderSimple) {
      CmsWorkOrderSimple wo = CmsWorkOrderSimple.class.cast(bwo);
      cloudName = wo.getCloud().getCiName();
    } else if (bwo instanceof CmsActionOrderSimple) {
      CmsActionOrderSimple ao = CmsActionOrderSimple.class.cast(bwo);
      cloudName = ao.getCloud().getCiName();
    }
    return cloudName;
  }


  public static Logger getLogger() {
    return logger;
  }

  public int getMinFreeSpaceMB() {
    return minFreeSpaceMB;
  }

  public List<String> getClouds() {
    return clouds;
  }

  public List<String> getBomClasses() {
    return bomClasses;
  }

  public List<String> getRfcActions() {
    return rfcActions;
  }

  public List<String> getStubbedCloudsList() {
    return stubbedCloudsList;
  }

  public void setStubbedCloudsList(List<String> stubbedCloudsList) {
    this.stubbedCloudsList = stubbedCloudsList;
  }

  public String getCircuitDir() {
    return circuitDir;
  }

  public String getInQueue() {
    return inQueue;
  }

  public int getRetryCount() {
    return retryCount;
  }

  public String getIpAttribute() {
    return ipAttribute;
  }

  public void setIpAttribute(String ipAttribute) {
    this.ipAttribute = ipAttribute;
  }

  public String getDataDir() {
    return dataDir;
  }

  public String getMgmtDomain() {
    return mgmtDomain;
  }

  public String getPerfCollectorCertLocation() {
    return perfCollectorCertLocation;
  }

  public String getMgmtUrl() {
    return mgmtUrl;
  }

  public String getMgmtCert() {
    return mgmtCert;
  }

  public String getDnsEnabled() {
    return dnsEnabled;
  }

  public boolean isDnsDisabled() {
    return dnsDisabled;
  }

  public String getDnsConfigFile() {
    return dnsConfigFile;
  }

  public String getDebugMode() {
    return debugMode;
  }

  public String getInitialUser() {
    return initialUser;
  }

  public int getLocalMaxConsumers() {
    return localMaxConsumers;
  }

  public int getRsyncTimeout() {
    return rsyncTimeout;
  }

  public String getPublicKey() {
    return publicKey;
  }

  public String getDnsKey() {
    return dnsKey;
  }

  public String getDnsSecret() {
    return dnsSecret;
  }

  public String getIpAddr() {
    return ipAddr;
  }

  public String getMgmtCertContent() {
    return mgmtCertContent;
  }

  public String getPerfCollectorCertContent() {
    return perfCollectorCertContent;
  }

  public String getEnv() {
    return env;
  }

  public void setEnv(String env) {
    this.env = env;
  }

  public void setCircuitDir(String circuitDir) {
    this.circuitDir = circuitDir;
  }

  public void setVerifyArgs(String verifyArgs) {
    this.verifyArgs = verifyArgs;
  }

  public String getVerifyArgs() {
    return verifyArgs;
  }

  public boolean isVerifyMode() {
    return verifyMode;
  }

  public void setVerifyMode(boolean verifyMode) {
    this.verifyMode = verifyMode;
  }

  public Map<String, String> getEnvVars() {
    return this.envVars;
  }

  public List<String> clouds() {
    return this.clouds;
  }

  public List<String> bomClasses() {
    return this.bomClasses;
  }

  public List<String> rfcActions() {
    return this.rfcActions;
  }

  public long getCmdTimeout() {
    return cmdTimeout;
  }

  public long getChefTimeout() {
    return chefTimeout;
  }

  public int getStubResponseTimeInSeconds() {
    return stubResponseTimeInSeconds;
  }

  public int getStubResultCode() {
    return stubResultCode;
  }

  public boolean isJMXEnabled() {
    return isJMXEnabled;
  }

  public void setJMXEnabled(boolean JMXEnabled) {
    isJMXEnabled = JMXEnabled;
  }

  public boolean isAutoShutDown() {
    return isAutoShutDown;
  }

  public void setAutoShutDown(boolean autoShutDown) {
    isAutoShutDown = autoShutDown;
  }

  public double getAutoShutDownThreshold() {
    return autoShutDownThreshold;
  }

  public int getRebootLimit() {
    return rebootLimit;
  }

  public void setRebootLimit(int rebootLimit) {
    this.rebootLimit = rebootLimit;
  }

  public void setDataDir(String dataDir) {
    this.dataDir = dataDir;
  }

  public void setClouds(List clouds) {
    this.clouds = clouds;
  }

  public List<String> getVerifyExcludePaths() {
    return verifyExcludePaths;
  }

  public void setVerifyExcludePaths(List<String> verifyExcludePaths) {
    this.verifyExcludePaths = verifyExcludePaths;
  }

  public void setChefTimeout(long chefTimeout) {
    this.chefTimeout = chefTimeout;
  }

  @Override
  public String toString() {
    return "Config{" +
        "circuitDir='" + circuitDir + '\'' +
        ", inQueue='" + inQueue + '\'' +
        ", retryCount=" + retryCount +
        ", ipAttribute='" + ipAttribute + '\'' +
        ", dataDir='" + dataDir + '\'' +
        ", mgmtDomain='" + mgmtDomain + '\'' +
        ", perfCollectorCertLocation='" + perfCollectorCertLocation + '\'' +
        ", mgmtUrl='" + mgmtUrl + '\'' +
        ", mgmtCert='" + mgmtCert + '\'' +
        ", minFreeSpaceMB=" + minFreeSpaceMB +
        ", dnsEnabled='" + dnsEnabled + '\'' +
        ", dnsDisabled=" + dnsDisabled +
        ", dnsConfigFile='" + dnsConfigFile + '\'' +
        ", debugMode='" + debugMode + '\'' +
        ", isJMXEnabled=" + isJMXEnabled +
        ", isAutoShutDown=" + isAutoShutDown +
        ", initialUser='" + initialUser + '\'' +
        ", localMaxConsumers=" + localMaxConsumers +
        ", rsyncTimeout=" + rsyncTimeout +
        ", stubbedCloudsList=" + stubbedCloudsList +
        ", clouds=" + clouds +
        ", stubResultCode=" + stubResultCode +
        ", stubResponseTimeInSeconds=" + stubResponseTimeInSeconds +
        ", autoShutDownThreshold=" + autoShutDownThreshold +
        ", bomClasses=" + bomClasses +
        ", rfcActions=" + rfcActions +
        ", cmdTimeout=" + cmdTimeout +
        ", chefTimeout=" + chefTimeout +
        ", env='" + env + '\'' +
        ", rebootLimit=" + rebootLimit +
        ", verifyMode=" + verifyMode +
        ", verifyExcludePaths=" + verifyExcludePaths +
        ", verifyArgs=" + verifyArgs +
        ", ipAddr='" + ipAddr + '\'' +
        '}';
  }
}