// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#include "opentelemetry/sdk/common/env_variables.h"

#ifdef _MSC_VER
#  include <string.h>
#  define strcasecmp _stricmp
#else
#  include <strings.h>
#endif

#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <limits>
#include <ostream>

#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/common/global_log_handler.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace common
{

static bool GetRawEnvironmentVariable(const char *env_var_name, std::string &value)
{
#if !defined(NO_GETENV)
  const char *endpoint_from_env = nullptr;
#  if defined(_MSC_VER)
  // avoid calling std::getenv which is deprecated in MSVC.
  size_t required_size = 0;
  getenv_s(&required_size, nullptr, 0, env_var_name);
  std::unique_ptr<char[]> endpoint_buffer;
  if (required_size > 0)
  {
    endpoint_buffer = std::unique_ptr<char[]>{new char[required_size]};
    getenv_s(&required_size, endpoint_buffer.get(), required_size, env_var_name);
    endpoint_from_env = endpoint_buffer.get();
  }
#  else
  endpoint_from_env = std::getenv(env_var_name);
#  endif  // defined(_MSC_VER)

  if (endpoint_from_env != nullptr)
  {
    value = std::string{endpoint_from_env};
    return true;
  }

  value = std::string{};
  return false;
#else
  value = std::string{};
  return false;
#endif  // !defined(NO_GETENV)
}

bool GetBoolEnvironmentVariable(const char *env_var_name, bool &value)
{
  std::string raw_value;
  bool exists = GetRawEnvironmentVariable(env_var_name, raw_value);
  if (!exists || raw_value.empty())
  {
    value = false;
    return false;
  }

  if (strcasecmp(raw_value.c_str(), "true") == 0)
  {
    value = true;
    return true;
  }

  if (strcasecmp(raw_value.c_str(), "false") == 0)
  {
    value = false;
    return true;
  }

  OTEL_INTERNAL_LOG_WARN("Environment variable <" << env_var_name << "> has an invalid value <"
                                                  << raw_value << ">, defaulting to false");
  value = false;
  return true;
}

static bool GetTimeoutFromString(const char *input, std::chrono::system_clock::duration &value)
{
  std::chrono::system_clock::duration::rep result = 0;

  // Skip spaces
  for (; *input && std::isspace(*input); ++input)
    ;

  for (; *input && std::isdigit(*input); ++input)
  {
    auto digit = (*input - '0');

    if (result > (std::numeric_limits<decltype(result)>::max() - digit) / 10)
    {
      // Rejecting overflow as invalid.
      return false;
    }
    result = result * 10 + digit;
  }

  if (result == 0)
  {
    // Rejecting duration 0 as invalid.
    return false;
  }

  opentelemetry::nostd::string_view unit{input};

  if (unit == "ns")
  {
    value = std::chrono::duration_cast<std::chrono::system_clock::duration>(
        std::chrono::nanoseconds{result});
    return true;
  }

  if (unit == "us")
  {
    value = std::chrono::duration_cast<std::chrono::system_clock::duration>(
        std::chrono::microseconds{result});
    return true;
  }

  if (unit == "ms")
  {
    value = std::chrono::duration_cast<std::chrono::system_clock::duration>(
        std::chrono::milliseconds{result});
    return true;
  }

  if (unit == "s")
  {
    value = std::chrono::duration_cast<std::chrono::system_clock::duration>(
        std::chrono::seconds{result});
    return true;
  }

  if (unit == "m")
  {
    value = std::chrono::duration_cast<std::chrono::system_clock::duration>(
        std::chrono::minutes{result});
    return true;
  }

  if (unit == "h")
  {
    value =
        std::chrono::duration_cast<std::chrono::system_clock::duration>(std::chrono::hours{result});
    return true;
  }

  if (unit == "")
  {
    // The spec says milliseconds, but opentelemetry-cpp implemented
    // seconds by default, up to opentelemetry-cpp 1.26.0.

    value = std::chrono::duration_cast<std::chrono::system_clock::duration>(
        std::chrono::milliseconds{result});
    return true;
  }

  // Failed to parse the input string.
  return false;
}

bool GetDurationEnvironmentVariable(const char *env_var_name,
                                    std::chrono::system_clock::duration &value)
{
  std::string raw_value;
  bool exists = GetRawEnvironmentVariable(env_var_name, raw_value);
  if (!exists || raw_value.empty())
  {
    value =
        std::chrono::duration_cast<std::chrono::system_clock::duration>(std::chrono::seconds{0});
    return false;
  }

  exists = GetTimeoutFromString(raw_value.c_str(), value);

  if (!exists)
  {
    OTEL_INTERNAL_LOG_WARN("Environment variable <" << env_var_name << "> has an invalid value <"
                                                    << raw_value << ">, ignoring");
  }
  return exists;
}

bool GetStringEnvironmentVariable(const char *env_var_name, std::string &value)
{
  bool exists = GetRawEnvironmentVariable(env_var_name, value);
  if (!exists || value.empty())
  {
    return false;
  }
  return true;
}

bool GetUintEnvironmentVariable(const char *env_var_name, std::uint32_t &value)
{
  static constexpr auto kDefaultValue = 0U;
  std::string raw_value;
  bool exists = GetRawEnvironmentVariable(env_var_name, raw_value);

  if (!exists || raw_value.empty())
  {
    value = kDefaultValue;
    return false;
  }

  const char *end  = raw_value.c_str() + raw_value.length();
  char *actual_end = nullptr;
  const auto temp  = std::strtoull(raw_value.c_str(), &actual_end, 10);

  if (errno == ERANGE)
  {
    errno = 0;
    OTEL_INTERNAL_LOG_WARN("Environment variable <" << env_var_name << "> is out of range <"
                                                    << raw_value << ">, defaulting to "
                                                    << kDefaultValue);
  }
  else if (actual_end != end || std::numeric_limits<std::uint32_t>::max() < temp)
  {
    OTEL_INTERNAL_LOG_WARN("Environment variable <" << env_var_name << "> has an invalid value <"
                                                    << raw_value << ">, defaulting to "
                                                    << kDefaultValue);
  }
  else
  {
    value = static_cast<std::uint32_t>(temp);
    return true;
  }

  value = kDefaultValue;
  return false;
}

bool GetFloatEnvironmentVariable(const char *env_var_name, float &value)
{
  static constexpr auto kDefaultValue = 0.0f;
  std::string raw_value;
  bool exists = GetRawEnvironmentVariable(env_var_name, raw_value);

  if (!exists || raw_value.empty())
  {
    value = kDefaultValue;
    return false;
  }

  const char *end  = raw_value.c_str() + raw_value.length();
  char *actual_end = nullptr;
  value            = std::strtof(raw_value.c_str(), &actual_end);

  if (errno == ERANGE)
  {
    errno = 0;
    OTEL_INTERNAL_LOG_WARN("Environment variable <" << env_var_name << "> is out of range <"
                                                    << raw_value << ">, defaulting to "
                                                    << kDefaultValue);
  }
  else if (actual_end != end)
  {
    OTEL_INTERNAL_LOG_WARN("Environment variable <" << env_var_name << "> has an invalid value <"
                                                    << raw_value << ">, defaulting to "
                                                    << kDefaultValue);
  }
  else
  {
    return true;
  }

  value = kDefaultValue;
  return false;
}

}  // namespace common
}  // namespace sdk
OPENTELEMETRY_END_NAMESPACE
