/*
 * 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.solr.core;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.apache.solr.cloud.CloudDescriptor;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.PropertiesUtil;
import org.apache.solr.common.util.StrUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Metadata about a {@link SolrCore}. It's mostly loaded from a file on disk at the very beginning
 * of loading a core.
 *
 * <p>It's mostly but not completely immutable; we should fix this!
 *
 * @since solr 1.3
 */
public class CoreDescriptor {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  // Properties file name constants
  public static final String CORE_NAME = "name";
  public static final String CORE_CONFIG = "config";
  public static final String CORE_DATADIR = "dataDir";
  public static final String CORE_ULOGDIR = "ulogDir";
  public static final String CORE_SCHEMA = "schema";
  public static final String CORE_SHARD = "shard";
  public static final String CORE_COLLECTION = "collection";
  public static final String CORE_ROLES = "roles";
  public static final String CORE_PROPERTIES = "properties";
  public static final String CORE_LOADONSTARTUP = "loadOnStartup";
  public static final String CORE_TRANSIENT = "transient";
  public static final String CORE_NODE_NAME = "coreNodeName";
  public static final String CORE_CONFIGSET = "configSet";
  public static final String CORE_CONFIGSET_PROPERTIES = "configSetProperties";
  public static final String SOLR_CORE_PROP_PREFIX = "solr.core.";

  public static final String DEFAULT_EXTERNAL_PROPERTIES_FILE =
      "conf" + File.separator + "solrcore.properties";

  /**
   * Whether this core was configured using a configSet that was trusted. This helps in avoiding the
   * loading of plugins that have potential vulnerabilities, when the configSet was not uploaded
   * from a trusted user.
   */
  private boolean trustedConfigSet = true;

  /**
   * Get the standard properties in persistable form
   *
   * @return the standard core properties in persistable form
   */
  public Properties getPersistableStandardProperties() {
    return originalCoreProperties;
  }

  /**
   * Get user-defined core properties in persistable form
   *
   * @return user-defined core properties in persistable form
   */
  public Properties getPersistableUserProperties() {
    return originalExtraProperties;
  }

  private static final Map<String, String> defaultProperties =
      Map.of(
          CORE_CONFIG, "solrconfig.xml",
          CORE_SCHEMA, "schema.xml",
          CORE_CONFIGSET_PROPERTIES, ConfigSetProperties.DEFAULT_FILENAME,
          CORE_DATADIR, "data" + File.separator,
          CORE_TRANSIENT, "false",
          CORE_LOADONSTARTUP, "true");

  private static final List<String> requiredProperties = List.of(CORE_NAME);

  public static List<String> standardPropNames =
      List.of(
          CORE_NAME,
          CORE_CONFIG,
          CORE_DATADIR,
          CORE_ULOGDIR,
          CORE_SCHEMA,
          CORE_PROPERTIES,
          CORE_CONFIGSET_PROPERTIES,
          CORE_LOADONSTARTUP,
          CORE_TRANSIENT,
          CORE_CONFIGSET,
          // cloud props
          CORE_SHARD,
          CORE_COLLECTION,
          CORE_ROLES,
          CORE_NODE_NAME,
          CloudDescriptor.NUM_SHARDS);

  private final CloudDescriptor cloudDesc;

  /** The absolute path to where the core lives. */
  private final Path instanceDir;

  /** The original standard core properties, before substitution */
  protected final Properties originalCoreProperties = new Properties();

  /** The original extra core properties, before substitution */
  protected final Properties originalExtraProperties = new Properties();

  /** The properties for this core, as available through getProperty() */
  protected final Properties coreProperties = new Properties();

  /** The properties for this core, substitutable by resource loaders */
  protected final Properties substitutableProperties = new Properties();

  /** TESTS ONLY */
  public CoreDescriptor(
      String name, Path instanceDir, CoreContainer coreContainer, String... coreProps) {
    this(
        name,
        instanceDir,
        toMap(coreProps),
        coreContainer.getContainerProperties(),
        coreContainer.getZkController());
  }

  private static Map<String, String> toMap(String... properties) {
    Map<String, String> props = new HashMap<>();
    assert properties.length % 2 == 0;
    for (int i = 0; i < properties.length; i += 2) {
      props.put(properties[i], properties[i + 1]);
    }
    return props;
  }

  /**
   * Create a new CoreDescriptor using the properties of an existing one
   *
   * @param coreName the new CoreDescriptor's name
   * @param other the CoreDescriptor to copy
   */
  public CoreDescriptor(String coreName, CoreDescriptor other) {
    this.cloudDesc = other.cloudDesc;
    this.instanceDir = other.instanceDir;
    this.originalExtraProperties.putAll(other.originalExtraProperties);
    this.originalCoreProperties.putAll(other.originalCoreProperties);
    this.coreProperties.putAll(other.coreProperties);
    this.substitutableProperties.putAll(other.substitutableProperties);
    this.coreProperties.setProperty(CORE_NAME, coreName);
    this.originalCoreProperties.setProperty(CORE_NAME, coreName);
    this.substitutableProperties.setProperty(SOLR_CORE_PROP_PREFIX + CORE_NAME, coreName);
    this.trustedConfigSet = other.trustedConfigSet;
  }

  /**
   * Create a new CoreDescriptor.
   *
   * @param name the CoreDescriptor's name
   * @param instanceDir a Path resolving to the instanceDir. Must be absolute.
   * @param coreProps a Map of the properties for this core
   * @param containerProperties the properties from the enclosing container.
   * @param zkController the ZkController in SolrCloud mode, otherwise null.
   */
  public CoreDescriptor(
      String name,
      Path instanceDir,
      Map<String, String> coreProps,
      Properties containerProperties,
      ZkController zkController) {
    this.instanceDir = instanceDir;
    assert instanceDir.isAbsolute();

    originalCoreProperties.setProperty(CORE_NAME, name);

    name =
        PropertiesUtil.substituteProperty(
            checkPropertyIsNotEmpty(name, CORE_NAME), containerProperties);

    coreProperties.putAll(defaultProperties);
    coreProperties.put(CORE_NAME, name);

    for (Map.Entry<String, String> entry : coreProps.entrySet()) {
      String propname = entry.getKey();
      String propvalue = entry.getValue();

      if (isUserDefinedProperty(propname)) originalExtraProperties.put(propname, propvalue);
      else originalCoreProperties.put(propname, propvalue);

      // Required props are already dealt with
      if (!requiredProperties.contains(propname)) {
        coreProperties.setProperty(
            propname, PropertiesUtil.substituteProperty(propvalue, containerProperties));
      }
    }

    loadExtraProperties();
    buildSubstitutableProperties();

    // TODO maybe make this a CloudCoreDescriptor subclass?
    if (zkController != null) {
      cloudDesc = new CloudDescriptor(this, name, coreProperties);
    } else {
      cloudDesc = null;
    }
    log.debug("Created CoreDescriptor: {}", coreProperties);
  }

  /**
   * Load properties specified in an external properties file.
   *
   * <p>The file to load can be specified in a {@code properties} property on the original
   * Properties object used to create this CoreDescriptor. If this has not been set, then we look
   * for {@code conf/solrcore.properties} underneath the instance dir.
   *
   * <p>File paths are taken as read from the core's instance directory if they are not absolute.
   */
  protected void loadExtraProperties() {
    String filename = coreProperties.getProperty(CORE_PROPERTIES, DEFAULT_EXTERNAL_PROPERTIES_FILE);
    Path propertiesFile = instanceDir.resolve(filename);
    if (Files.exists(propertiesFile)) {
      try (Reader r = Files.newBufferedReader(propertiesFile, StandardCharsets.UTF_8)) {
        Properties externalProps = new Properties();
        externalProps.load(r);
        coreProperties.putAll(externalProps);
      } catch (IOException e) {
        String message =
            String.format(Locale.ROOT, "Could not load properties from %s: %s:", propertiesFile, e);
        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, message);
      }
    }
  }

  /**
   * Create the properties object used by resource loaders, etc., for property substitution. The
   * default solr properties are prefixed with 'solr.core.', so, e.g., 'name' becomes
   * 'solr.core.name'
   */
  protected void buildSubstitutableProperties() {
    for (String propName : coreProperties.stringPropertyNames()) {
      String propValue = coreProperties.getProperty(propName);
      if (!isUserDefinedProperty(propName)) propName = SOLR_CORE_PROP_PREFIX + propName;
      substitutableProperties.setProperty(propName, propValue);
    }
    substitutableProperties.setProperty("solr.core.instanceDir", instanceDir.toString());
  }

  /**
   * Is this property a Solr-standard property, or is it an extra property defined per-core by the
   * user?
   *
   * @param propName the Property name
   * @return {@code true} if this property is user-defined
   */
  protected static boolean isUserDefinedProperty(String propName) {
    return !standardPropNames.contains(propName);
  }

  public static String checkPropertyIsNotEmpty(String value, String propName) {
    if (StrUtils.isNullOrEmpty(value)) {
      String message =
          String.format(Locale.ROOT, "Cannot create core with empty %s value", propName);
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, message);
    }
    return value;
  }

  public String getPropertiesName() {
    return coreProperties.getProperty(CORE_PROPERTIES);
  }

  public String getDataDir() {
    return coreProperties.getProperty(CORE_DATADIR);
  }

  public boolean usingDefaultDataDir() {
    return defaultProperties.get(CORE_DATADIR).equals(coreProperties.getProperty(CORE_DATADIR));
  }

  /** The core instance directory (absolute). */
  public Path getInstanceDir() {
    return instanceDir;
  }

  /**
   * @return the core configuration resource name.
   */
  public String getConfigName() {
    return coreProperties.getProperty(CORE_CONFIG);
  }

  /**
   * @return the core schema resource name. Not actually used if schema is managed mode.
   */
  public String getSchemaName() {
    return coreProperties.getProperty(CORE_SCHEMA);
  }

  /**
   * @return the initial core name
   */
  public String getName() {
    return coreProperties.getProperty(CORE_NAME);
  }

  /** TODO remove mutability */
  void setProperty(String prop, String val) {
    if (substitutableProperties.containsKey(prop)) {
      substitutableProperties.setProperty(prop, val);
      return;
    }
    coreProperties.setProperty(prop, val);
  }

  public String getCollectionName() {
    return cloudDesc == null ? null : cloudDesc.getCollectionName();
  }

  public CloudDescriptor getCloudDescriptor() {
    return cloudDesc;
  }

  public boolean isLoadOnStartup() {
    String tmp = coreProperties.getProperty(CORE_LOADONSTARTUP, "false");
    return Boolean.parseBoolean(tmp);
  }

  public boolean isTransient() {
    String tmp = coreProperties.getProperty(CORE_TRANSIENT, "false");
    return PropertiesUtil.toBoolean(tmp);
  }

  public String getUlogDir() {
    return coreProperties.getProperty(CORE_ULOGDIR);
  }

  /**
   * Returns a specific property defined on this CoreDescriptor
   *
   * @param prop - value to read from the properties structure.
   * @param defVal - return if no property found.
   * @return associated string. May be null.
   */
  public String getCoreProperty(String prop, String defVal) {
    return coreProperties.getProperty(prop, defVal);
  }

  /**
   * Returns all substitutable properties defined on this CoreDescriptor
   *
   * @return all substitutable properties defined on this CoreDescriptor
   */
  public Properties getSubstitutableProperties() {
    return substitutableProperties;
  }

  @Override
  public String toString() {
    return "CoreDescriptor[name=" + this.getName() + ";instanceDir=" + this.getInstanceDir() + "]";
  }

  public String getConfigSet() {
    // TODO consider falling back on "collection.configName" ( CollectionAdminParams.COLL_CONF )
    return coreProperties.getProperty(CORE_CONFIGSET);
  }

  /** TODO remove mutability or at least make this non-public? */
  public void setConfigSet(String configSetName) {
    coreProperties.setProperty(CORE_CONFIGSET, configSetName);
  }

  public String getConfigSetPropertiesName() {
    return coreProperties.getProperty(CORE_CONFIGSET_PROPERTIES);
  }

  public boolean isConfigSetTrusted() {
    return trustedConfigSet;
  }

  /** TODO remove mutability */
  public void setConfigSetTrusted(boolean trusted) {
    this.trustedConfigSet = trusted;
  }
}
