/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 */

#include <folly/logging/LogConfigParser.h>

#include <folly/Conv.h>
#include <folly/String.h>
#include <folly/dynamic.h>
#include <folly/json.h>
#include <folly/lang/SafeAssert.h>
#include <folly/logging/LogName.h>

using std::string;

namespace folly {

namespace {

/**
 * Get the type of a folly::dynamic object as a string, for inclusion in
 * exception messages.
 */
std::string dynamicTypename(const dynamic& value) {
  switch (value.type()) {
    case dynamic::NULLT:
      return "null";
    case dynamic::ARRAY:
      return "array";
    case dynamic::BOOL:
      return "boolean";
    case dynamic::DOUBLE:
      return "double";
    case dynamic::INT64:
      return "integer";
    case dynamic::OBJECT:
      return "object";
    case dynamic::STRING:
      return "string";
  }
  return "unknown type";
}

/**
 * Parse a LogLevel from a JSON value.
 *
 * This accepts the log level either as an integer value or a string that can
 * be parsed with stringToLogLevel()
 *
 * On success updates the result parameter and returns true.
 * Returns false if the input is not a string or integer.
 * Throws a LogConfigParseError on other errors.
 */
bool parseJsonLevel(
    const dynamic& value, StringPiece categoryName, LogLevel& result) {
  if (value.isString()) {
    auto levelString = value.asString();
    try {
      result = stringToLogLevel(levelString);
      return true;
    } catch (const std::exception&) {
      throw LogConfigParseError{to<string>(
          "invalid log level \"",
          levelString,
          "\" for category \"",
          categoryName,
          "\"")};
    }
  } else if (value.isInt()) {
    auto level = static_cast<LogLevel>(value.asInt());
    if (level < LogLevel::MIN_LEVEL || level > LogLevel::MAX_LEVEL) {
      throw LogConfigParseError{to<string>(
          "invalid log level ",
          value.asInt(),
          " for category \"",
          categoryName,
          "\": outside of valid range")};
    }
    result = level;
    return true;
  }

  return false;
}

LogCategoryConfig parseJsonCategoryConfig(
    const dynamic& value, StringPiece categoryName) {
  LogCategoryConfig config;

  // If the input is not an object, allow it to be
  // just a plain level specification
  if (!value.isObject()) {
    if (!parseJsonLevel(value, categoryName, config.level)) {
      throw LogConfigParseError{to<string>(
          "unexpected data type for configuration of category \"",
          categoryName,
          "\": got ",
          dynamicTypename(value),
          ", expected an object, string, or integer")};
    }
    return config;
  }

  auto* level = value.get_ptr("level");
  if (!level) {
    // Require that level information be present for each log category.
    throw LogConfigParseError{to<string>(
        "no log level specified for category \"", categoryName, "\"")};
  }
  if (!parseJsonLevel(*level, categoryName, config.level)) {
    throw LogConfigParseError{to<string>(
        "unexpected data type for level field of category \"",
        categoryName,
        "\": got ",
        dynamicTypename(*level),
        ", expected a string or integer")};
  }

  auto* inherit = value.get_ptr("inherit");
  if (inherit) {
    if (!inherit->isBool()) {
      throw LogConfigParseError{to<string>(
          "unexpected data type for inherit field of category \"",
          categoryName,
          "\": got ",
          dynamicTypename(*inherit),
          ", expected a boolean")};
    }
    config.inheritParentLevel = inherit->asBool();
  }

  auto* propagate = value.get_ptr("propagate");
  if (propagate) {
    if (!parseJsonLevel(
            *propagate, categoryName, config.propagateLevelMessagesToParent)) {
      throw LogConfigParseError{to<string>(
          "unexpected data type for propagate field of category \"",
          categoryName,
          "\": got ",
          dynamicTypename(*propagate),
          ", expected a string or integer")};
    }
  }

  auto* handlers = value.get_ptr("handlers");
  if (handlers) {
    if (!handlers->isArray()) {
      throw LogConfigParseError{to<string>(
          "the \"handlers\" field for category ",
          categoryName,
          " must be a list")};
    }
    config.handlers = std::vector<std::string>{};
    for (const auto& item : *handlers) {
      if (!item.isString()) {
        throw LogConfigParseError{to<string>(
            "the \"handlers\" list for category ",
            categoryName,
            " must be contain only strings")};
      }
      config.handlers->push_back(item.asString());
    }
  }

  return config;
}

LogHandlerConfig parseJsonHandlerConfig(
    const dynamic& value, StringPiece handlerName) {
  if (!value.isObject()) {
    throw LogConfigParseError{to<string>(
        "unexpected data type for configuration of handler \"",
        handlerName,
        "\": got ",
        dynamicTypename(value),
        ", expected an object")};
  }

  // Parse the handler type
  auto* type = value.get_ptr("type");
  if (!type) {
    throw LogConfigParseError{to<string>(
        "no handler type specified for log handler \"", handlerName, "\"")};
  }
  if (!type->isString()) {
    throw LogConfigParseError{to<string>(
        "unexpected data type for \"type\" field of handler \"",
        handlerName,
        "\": got ",
        dynamicTypename(*type),
        ", expected a string")};
  }
  LogHandlerConfig config{type->asString()};

  // Parse the handler options
  auto* options = value.get_ptr("options");
  if (options) {
    if (!options->isObject()) {
      throw LogConfigParseError{to<string>(
          "unexpected data type for \"options\" field of handler \"",
          handlerName,
          "\": got ",
          dynamicTypename(*options),
          ", expected an object")};
    }

    for (const auto& item : options->items()) {
      if (!item.first.isString()) {
        // This shouldn't really ever happen.
        // We deserialize the json with allow_non_string_keys set to False.
        throw LogConfigParseError{to<string>(
            "unexpected data type for option of handler \"",
            handlerName,
            "\": got ",
            dynamicTypename(item.first),
            ", expected string")};
      }
      if (!item.second.isString()) {
        throw LogConfigParseError{to<string>(
            "unexpected data type for option \"",
            item.first.asString(),
            "\" of handler \"",
            handlerName,
            "\": got ",
            dynamicTypename(item.second),
            ", expected a string")};
      }
      config.options[item.first.asString()] = item.second.asString();
    }
  }

  return config;
}

LogConfig::CategoryConfigMap parseCategoryConfigs(StringPiece value) {
  LogConfig::CategoryConfigMap categoryConfigs;

  // Allow empty (or all whitespace) input
  value = trimWhitespace(value);
  if (value.empty()) {
    return categoryConfigs;
  }

  std::unordered_map<string, string> seenCategories;
  std::vector<StringPiece> pieces;
  folly::split(",", value, pieces);
  for (const auto& piece : pieces) {
    LogCategoryConfig categoryConfig;
    StringPiece categoryName;
    StringPiece configString;

    auto equalIndex = piece.find('=');
    if (equalIndex == StringPiece::npos) {
      // If level information is supplied without a category name,
      // apply it to the root log category.
      categoryName = StringPiece{"."};
      configString = trimWhitespace(piece);
    } else {
      categoryName = piece.subpiece(0, equalIndex);
      configString = piece.subpiece(equalIndex + 1);

      // If ":=" is used instead of just "=", disable inheriting the parent's
      // effective level if it is lower than this category's level.
      if (categoryName.endsWith(':')) {
        categoryConfig.inheritParentLevel = false;
        categoryName.subtract(1);
      }

      // Remove whitespace from the category name
      categoryName = trimWhitespace(categoryName);
    }

    // Split the configString into level and handler information.
    std::vector<StringPiece> handlerPieces;
    folly::split(":", configString, handlerPieces);
    FOLLY_SAFE_DCHECK(
        !handlerPieces.empty(),
        "folly::split() always returns a list of length 1");
    auto levelString = trimWhitespace(handlerPieces[0]);

    bool hasHandlerConfig = handlerPieces.size() > 1;
    if (handlerPieces.size() == 2 && trimWhitespace(handlerPieces[1]).empty()) {
      // This is an explicitly empty handler list.
      // This requests LoggerDB::updateConfig() to clear all existing log
      // handlers from this category.
      categoryConfig.handlers = std::vector<std::string>{};
    } else if (hasHandlerConfig) {
      categoryConfig.handlers = std::vector<std::string>{};
      for (size_t n = 1; n < handlerPieces.size(); ++n) {
        auto handlerName = trimWhitespace(handlerPieces[n]);
        if (handlerName.empty()) {
          throw LogConfigParseError{to<string>(
              "error parsing configuration for log category \"",
              categoryName,
              "\": log handler name cannot be empty")};
        }
        categoryConfig.handlers->push_back(handlerName.str());
      }
    }

    // Parse the levelString into a LogLevel
    levelString = trimWhitespace(levelString);
    try {
      categoryConfig.level = stringToLogLevel(levelString);
    } catch (const std::exception&) {
      throw LogConfigParseError{to<string>(
          "invalid log level \"",
          levelString,
          "\" for category \"",
          categoryName,
          "\"")};
    }

    // Check for multiple entries for the same category with different but
    // equivalent names.
    auto canonicalName = LogName::canonicalize(categoryName);
    auto ret = seenCategories.emplace(canonicalName, categoryName.str());
    if (!ret.second) {
      throw LogConfigParseError{to<string>(
          "category \"",
          canonicalName,
          "\" listed multiple times under different names: \"",
          ret.first->second,
          "\" and \"",
          categoryName,
          "\"")};
    }

    auto emplaceResult =
        categoryConfigs.emplace(canonicalName, std::move(categoryConfig));
    FOLLY_SAFE_DCHECK(
        emplaceResult.second,
        "category name must be new since it was not in seenCategories");
  }

  return categoryConfigs;
}

bool splitNameValue(
    StringPiece input, StringPiece* outName, StringPiece* outValue) {
  size_t equalIndex = input.find('=');
  if (equalIndex == StringPiece::npos) {
    return false;
  }

  StringPiece name{input.begin(), input.begin() + equalIndex};
  StringPiece value{input.begin() + equalIndex + 1, input.end()};

  *outName = trimWhitespace(name);
  *outValue = trimWhitespace(value);
  return true;
}

std::pair<std::string, LogHandlerConfig> parseHandlerConfig(StringPiece value) {
  // Parse the handler name and optional type
  auto colonIndex = value.find(':');
  StringPiece namePortion;
  StringPiece optionsStr;
  if (colonIndex == StringPiece::npos) {
    namePortion = value;
  } else {
    namePortion = StringPiece{value.begin(), value.begin() + colonIndex};
    optionsStr = StringPiece{value.begin() + colonIndex + 1, value.end()};
  }

  StringPiece handlerName;
  Optional<StringPiece> handlerType(in_place);
  if (!splitNameValue(namePortion, &handlerName, &handlerType.value())) {
    handlerName = trimWhitespace(namePortion);
    handlerType = folly::none;
  }

  // Make sure the handler name and type are not empty.
  // Also disallow commas in the name: this helps catch accidental errors where
  // the user left out the ':' and intended to be specifying options instead of
  // part of the name or type.
  if (handlerName.empty()) {
    throw LogConfigParseError{
        "error parsing log handler configuration: empty log handler name"};
  }
  if (handlerName.contains(',')) {
    throw LogConfigParseError{to<string>(
        "error parsing configuration for log handler \"",
        handlerName,
        "\": name cannot contain a comma when using the basic config format")};
  }
  if (handlerType.has_value()) {
    if (handlerType->empty()) {
      throw LogConfigParseError{to<string>(
          "error parsing configuration for log handler \"",
          handlerName,
          "\": empty log handler type")};
    }
    if (handlerType->contains(',')) {
      throw LogConfigParseError{to<string>(
          "error parsing configuration for log handler \"",
          handlerName,
          "\": invalid type \"",
          handlerType.value(),
          "\": type name cannot contain a comma when using "
          "the basic config format")};
    }
  }

  // Parse the options
  LogHandlerConfig config{handlerType};
  optionsStr = trimWhitespace(optionsStr);
  if (!optionsStr.empty()) {
    std::vector<StringPiece> pieces;
    folly::split(",", optionsStr, pieces);
    FOLLY_SAFE_DCHECK(
        !pieces.empty(), "folly::split() always returns a list of length 1");

    for (const auto& piece : pieces) {
      StringPiece optionName;
      StringPiece optionValue;
      if (!splitNameValue(piece, &optionName, &optionValue)) {
        throw LogConfigParseError{to<string>(
            "error parsing configuration for log handler \"",
            handlerName,
            "\": options must be of the form NAME=VALUE")};
      }

      auto ret = config.options.emplace(optionName.str(), optionValue.str());
      if (!ret.second) {
        throw LogConfigParseError{to<string>(
            "error parsing configuration for log handler \"",
            handlerName,
            "\": duplicate configuration for option \"",
            optionName,
            "\"")};
      }
    }
  }

  return std::make_pair(handlerName.str(), std::move(config));
}

} // namespace

LogConfig parseLogConfig(StringPiece value) {
  value = trimWhitespace(value);
  if (value.startsWith('{')) {
    return parseLogConfigJson(value);
  }

  // Split the input string on semicolons.
  // Everything up to the first semicolon specifies log category configs.
  // From then on each section specifies a single LogHandler config.
  std::vector<StringPiece> pieces;
  folly::split(";", value, pieces);
  FOLLY_SAFE_DCHECK(
      !pieces.empty(), "folly::split() always returns a list of length 1");

  auto categoryConfigs = parseCategoryConfigs(pieces[0]);
  LogConfig::HandlerConfigMap handlerConfigs;
  for (size_t n = 1; n < pieces.size(); ++n) {
    auto handlerInfo = parseHandlerConfig(pieces[n]);
    auto ret = handlerConfigs.emplace(
        handlerInfo.first, std::move(handlerInfo.second));
    if (!ret.second) {
      throw LogConfigParseError{to<string>(
          "configuration for log category \"",
          handlerInfo.first,
          "\" specified multiple times")};
    }
  }

  return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
}

LogConfig parseLogConfigJson(StringPiece value) {
  json::serialization_opts opts;
  opts.allow_trailing_comma = true;
  auto jsonData = folly::parseJson(json::stripComments(value), opts);
  return parseLogConfigDynamic(jsonData);
}

LogConfig parseLogConfigDynamic(const dynamic& value) {
  if (!value.isObject()) {
    throw LogConfigParseError{"JSON config input must be an object"};
  }

  std::unordered_map<string, string> seenCategories;
  LogConfig::CategoryConfigMap categoryConfigs;
  auto* categories = value.get_ptr("categories");
  if (categories) {
    if (!categories->isObject()) {
      throw LogConfigParseError{to<string>(
          "unexpected data type for log categories config: got ",
          dynamicTypename(*categories),
          ", expected an object")};
    }

    for (const auto& entry : categories->items()) {
      if (!entry.first.isString()) {
        // This shouldn't really ever happen.
        // We deserialize the json with allow_non_string_keys set to False.
        throw LogConfigParseError{"category name must be a string"};
      }
      auto categoryName = entry.first.asString();
      auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);

      // Check for multiple entries for the same category with different but
      // equivalent names.
      auto canonicalName = LogName::canonicalize(categoryName);
      auto ret = seenCategories.emplace(canonicalName, categoryName);
      if (!ret.second) {
        throw LogConfigParseError{to<string>(
            "category \"",
            canonicalName,
            "\" listed multiple times under different names: \"",
            ret.first->second,
            "\" and \"",
            categoryName,
            "\"")};
      }

      categoryConfigs[canonicalName] = std::move(categoryConfig);
    }
  }

  LogConfig::HandlerConfigMap handlerConfigs;
  auto* handlers = value.get_ptr("handlers");
  if (handlers) {
    if (!handlers->isObject()) {
      throw LogConfigParseError{to<string>(
          "unexpected data type for log handlers config: got ",
          dynamicTypename(*handlers),
          ", expected an object")};
    }

    for (const auto& entry : handlers->items()) {
      if (!entry.first.isString()) {
        // This shouldn't really ever happen.
        // We deserialize the json with allow_non_string_keys set to False.
        throw LogConfigParseError{"handler name must be a string"};
      }
      auto handlerName = entry.first.asString();
      handlerConfigs.emplace(
          handlerName, parseJsonHandlerConfig(entry.second, handlerName));
    }
  }

  return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
}

dynamic logConfigToDynamic(const LogConfig& config) {
  dynamic categories = dynamic::object;
  for (const auto& entry : config.getCategoryConfigs()) {
    categories.insert(entry.first, logConfigToDynamic(entry.second));
  }

  dynamic handlers = dynamic::object;
  for (const auto& entry : config.getHandlerConfigs()) {
    handlers.insert(entry.first, logConfigToDynamic(entry.second));
  }

  return dynamic::object("categories", std::move(categories))(
      "handlers", std::move(handlers));
}

dynamic logConfigToDynamic(const LogHandlerConfig& config) {
  dynamic options = dynamic::object;
  for (const auto& opt : config.options) {
    options.insert(opt.first, opt.second);
  }
  auto result = dynamic::object("options", options);
  if (config.type.has_value()) {
    result("type", config.type.value());
  }
  return result;
}

dynamic logConfigToDynamic(const LogCategoryConfig& config) {
  auto value = dynamic::object("level", logLevelToString(config.level))(
      "inherit", config.inheritParentLevel)(
      "propagate", logLevelToString(config.propagateLevelMessagesToParent));
  if (config.handlers.has_value()) {
    auto handlers = dynamic::array();
    for (const auto& handlerName : config.handlers.value()) {
      handlers.push_back(handlerName);
    }
    value("handlers", std::move(handlers));
  }
  return value;
}

} // namespace folly
