/*
 * SPDX-FileCopyrightText: 2024 Copyright contributors to the cppTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */
#include <tango/common/telemetry/Configuration.h>

#include <tango/internal/utils.h>

#include <tango/client/ApiUtil.h>
#include <tango/common/telemetry/consts.h>
#include <tango/common/utils/assert.h>
#include <tango/server/Except.h>
#include <tango/server/Logging.h>

#include <regex>
#include <string>
#include <variant>

namespace
{

std::vector<std::string> split_str_var(const std::string &str_var, const std::string delimiter = ",")
{
    // We allow for spaces before/after the comma separators, e.g., [logging, tracing] or [logging , tracing]
    std::regex regex{"\\s*" + delimiter + "\\s*"};
    return {std::sregex_token_iterator(std::begin(str_var), std::end(str_var), regex, -1),
            std::sregex_token_iterator()};
}

} // namespace

namespace Tango::telemetry
{

std::string to_string(Configuration::Exporter exporter_type)
{
    switch(exporter_type)
    {
    case Configuration::Exporter::grpc:
        return "grpc";
    case Configuration::Exporter::http:
        return "http";
    case Configuration::Exporter::console:
        return "console";
    default:
        using ut = std::underlying_type_t<Configuration::Exporter>;
        return std::to_string(static_cast<ut>(exporter_type));
    }
}

std::string to_string(Configuration::TelemetryType telemetry_type)
{
    switch(telemetry_type)
    {
    case Configuration::TelemetryType::logging:
        return "logging";
    case Configuration::TelemetryType::tracing:
        return "tracing";
    case Configuration::TelemetryType::none:
        return "none";
    default:
        using ut = std::underlying_type_t<Configuration::TelemetryType>;
        return std::to_string(static_cast<ut>(telemetry_type));
    }
}

std::string to_string(Configuration::TelemetryTopic topic)
{
    std::string topic_str;
    switch(topic)
    {
    case Configuration::TelemetryTopic::all:
        topic_str = "all";
        break;
    case Configuration::TelemetryTopic::database:
        topic_str = "database";
        break;
    case Configuration::TelemetryTopic::events:
        topic_str = "events";
        break;
    case Configuration::TelemetryTopic::polling:
        topic_str = "polling";
        break;
    case Configuration::TelemetryTopic::user:
        topic_str = "user";
        break;
    default:
        TANGO_ASSERT_ON_DEFAULT(topic);
    }
    return topic_str;
}

//-----------------------------------------------------------------------------------------
//! The default endpoint to which traces are exported
//-----------------------------------------------------------------------------------------
const std::string Configuration::DEFAULT_GRPC_TRACES_ENDPOINT{"grpc://localhost:4317"};

//-----------------------------------------------------------------------------------------
//! The default endpoint to which traces are exported
//-----------------------------------------------------------------------------------------
const std::string Configuration::DEFAULT_HTTP_TRACES_ENDPOINT{"http://localhost:4318/v1/traces"};

const std::string Configuration::DEFAULT_CONSOLE_TRACES_ENDPOINT{"cout"};

//-----------------------------------------------------------------------------------------
//! The default endpoint to which logs are exported
//-----------------------------------------------------------------------------------------
const std::string Configuration::DEFAULT_GRPC_LOGS_ENDPOINT{"grpc://localhost:4317"};

//-----------------------------------------------------------------------------------------
//! The default endpoint to which logs are exported
//-----------------------------------------------------------------------------------------
const std::string Configuration::DEFAULT_HTTP_LOGS_ENDPOINT{"http://localhost:4318/v1/logs"};

const std::string Configuration::DEFAULT_CONSOLE_LOGS_ENDPOINT{"cout"};

//-----------------------------------------------------------------------------------------
//! The default batch size for traces
//-----------------------------------------------------------------------------------------
const std::size_t Configuration::DEFAULT_TRACES_BATCH_SIZE = 512;

//-----------------------------------------------------------------------------------------
//! The default batch size for logs
//-----------------------------------------------------------------------------------------
const std::size_t Configuration::DEFAULT_LOGS_BATCH_SIZE = 512;

//-----------------------------------------------------------------------------------------
//! The default max batch queue size (threshold above which signals are dropped)
//-----------------------------------------------------------------------------------------
const std::size_t Configuration::DEFAULT_MAX_BATCH_QUEUE_SIZE = 2048;

//-----------------------------------------------------------------------------------------
//! The default delay (in ms) after which a batch processing is scheduled whatever is the
//! number of pending signals in the queue: 2500
//-----------------------------------------------------------------------------------------
const std::size_t Configuration::DEFAULT_BATCH_SCHEDULE_DELAY = 2500;

Configuration::Configuration(std::string id, std::string name_space, ServerClientDetails details) :
    id(id),
    name_space(name_space),
    details(details)
{
    auto *detailsServer = std::get_if<Configuration::Server>(&details);

    if(detailsServer != nullptr && detailsServer->class_name == "DServer")
    {
        enabled = false;
    }
    else
    {
        enabled = detail::get_boolean_env_var(Tango::telemetry::kEnvVarTelemetryEnable, false);
    }

    enable_telemetry_types_from_env(telemetry::kEnvVarTelemetryTypes);
    set_topics_from_env(telemetry::kEnvVarTelemetryTopics);
    set_tracing_collectors_from_env(telemetry::kEnvVarTelemetryTracingExporters, kEnvVarTelemetryTracingEndPoints);
    set_logging_collectors_from_env(telemetry::kEnvVarTelemetryLoggingExporters, kEnvVarTelemetryLoggingEndPoints);
}

Configuration::Configuration() :
    Configuration{"TangoTelemetry", "tango", Configuration::Client{"tango.telemetry.default"}}
{
}

Configuration::Kind Configuration::get_kind() const noexcept
{
    if(std::get_if<Configuration::Server>(&details) != nullptr)
    {
        return Configuration::Kind::Server;
    }
    return Configuration::Kind::Client;
}

bool Configuration::is_a(const Configuration::Kind &kind) const noexcept
{
    return get_kind() == kind;
}

bool Configuration::is_valid_http_endpoint(const std::string &endpoint) noexcept
{
    const std::regex pattern("^(http|https)://[^/]+:\\d+(/.*)?$");
    return std::regex_match(endpoint, pattern);
}

bool Configuration::is_valid_console_endpoint(const std::string &endpoint) noexcept
{
    const std::regex pattern("^(cout|cerr)$");
    return std::regex_match(endpoint, pattern);
}

bool Configuration::is_valid_grpc_endpoint(const std::string &endpoint) noexcept
{
    // regex pattern to match 'host:port'
    const std::regex pattern("^grpc://[^/]+:\\d+$");
    return std::regex_match(endpoint, pattern);
}

std::string Configuration::extract_grpc_host_port(const std::string &endpoint) const noexcept
{
    // regex pattern to match and capture 'host:port' from 'grpc://host:port'
    const std::regex pattern("^(?:grpc://)?([^/]+:\\d+)$");
    std::smatch matches;
    if(std::regex_search(endpoint, matches, pattern) && matches.size() > 1)
    {
        // valid endpoint return: host:port
        return matches[1].str();
    }
    else
    {
        // invalid endpoint return: empty string
        return "";
    }
}

/// @throw Tango::API_InvalidArgs
Configuration::Exporter Configuration::to_exporter(std::string_view str)
{
    if(str == "grpc")
    {
        return Exporter::grpc;
    }
    else if(str == "http")
    {
        return Exporter::http;
    }
    else if(str == "console")
    {
        return Exporter::console;
    }

    std::stringstream sstr;
    sstr << "Cannot parse '" << str << "' as Exporter enum class.";
    TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, sstr.str());
}

/// @throw Tango::API_InvalidArgs
Configuration::TelemetryType Configuration::to_type(std::string_view str)
{
    if(str == "logging")
    {
        return TelemetryType::logging;
    }
    else if(str == "tracing")
    {
        return TelemetryType::tracing;
    }
    else if(str == "none")
    {
        return TelemetryType::none;
    }

    std::stringstream sstr;
    sstr << "Cannot parse '" << str << "' as a telemetry type (e.g., 'logging' or 'tracing').";
    TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, sstr.str());
}

/// @throw Tango::API_InvalidArgs
Configuration::TelemetryTopic Configuration::to_topic(const std::string &str)
{
    if(str == "all")
    {
        return TelemetryTopic::all;
    }
    else if(str == "database")
    {
        return TelemetryTopic::database;
    }
    else if(str == "events")
    {
        return TelemetryTopic::events;
    }
    else if(str == "polling")
    {
        return TelemetryTopic::polling;
    }
    else if(str == "user")
    {
        return TelemetryTopic::user;
    }

    std::stringstream sstr;
    sstr << "Cannot parse '" << str << "' as a telemetry topic (e.g., 'all', 'database', 'event', 'polling', 'user').";
    TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, sstr.str());
}

void Configuration::enable_telemetry_types_from_env(const char *types_env_var)
{
    std::string env_str;

    int ret = ApiUtil::instance()->get_env_var(types_env_var, env_str);
    if(ret == 0)
    {
        enable_telemetry_types(env_str);
    }
    else
    {
        // Set the types to default to 'enabled' if no environment variable is set
        logging_enabled = true;
        tracing_enabled = true;
    }
}

void Configuration::enable_telemetry_types(const std::string &input_str)
{
    logging_enabled = false;
    tracing_enabled = false;
    for(auto const &type_str : split_str_var(input_str))
    {
        auto telemetry_type = to_type(type_str);
        switch(telemetry_type)
        {
        case TelemetryType::logging:
            logging_enabled = true;
            break;
        case TelemetryType::tracing:
            tracing_enabled = true;
            break;
        case TelemetryType::none:
            tracing_enabled = false;
            logging_enabled = false;
            return;
        default:
            TANGO_ASSERT_ON_DEFAULT(telemetry_type);
        }
    }
}

void Configuration::set_topics_from_env(const char *topics_env_var)
{
    std::string env_str;

    int ret = ApiUtil::instance()->get_env_var(topics_env_var, env_str);
    if(ret == 0)
    {
        set_topics(env_str);
    }
    else
    {
        // Set the topics to default to 'all' if no environment variable is set
        topics = {Configuration::TelemetryTopic::all};
    }
}

void Configuration::set_topics(const std::string &input_str)
{
    set_topics(split_str_var(input_str));
}

void Configuration::set_topics(const std::vector<std::string> &input_topics)
{
    topics.clear();
    for(auto const &topic_str : input_topics)
    {
        topics.insert(to_topic(topic_str));
    }
}

std::vector<std::string> Configuration::get_topics() const
{
    std::vector<std::string> ret;
    for(auto const &topic : topics)
    {
        ret.push_back(to_string(topic));
    }
    return ret;
}

bool Configuration::check_for_topic(const std::string &input_str) const
{
    std::regex delimiter{"\\s*,\\s*"};
    std::sregex_token_iterator topic_tokens{input_str.begin(), input_str.end(), delimiter, -1};
    std::sregex_token_iterator end;
    for(; topic_tokens != end; topic_tokens++)
    {
        if(check_for_topic(to_topic(*topic_tokens)))
        {
            return true;
        }
    }
    return false;
}

bool Configuration::check_for_topic(Configuration::TelemetryTopic topic) const
{
    return topics.find(topic) != topics.end();
}

Configuration::Collector Configuration::create_collector_from_endpoint(Exporter exporter, std::string endpoint)
{
    validate_exporter_and_endpoint(exporter, endpoint);
    return Configuration::Collector(exporter, endpoint);
}

Configuration::Collector Configuration::create_collector_from_endpoint(const std::string &exporter_str,
                                                                       std::string endpoint)
{
    auto exporter = to_exporter(exporter_str);
    return create_collector_from_endpoint(exporter, endpoint);
}

void Configuration::set_tracing_collectors(const std::string &exporters_str,
                                           const std::string &endpoints_str,
                                           const std::string &source)
{
    std::vector<Exporter> exporters;
    std::vector<std::string> endpoints;

    tracing_collectors.clear();
    convert_exporter_and_endpoint_strings(exporters_str, endpoints_str, source, exporters, endpoints);

    if(exporters.size() != endpoints.size())
    {
        endpoints.reserve(exporters.size());
        std::transform(exporters.begin(), exporters.end(), back_inserter(endpoints), get_tracing_default_endpoint);
    }
    for(unsigned int i = 0; i < exporters.size(); ++i)
    {
        tracing_collectors.push_back(create_collector_from_endpoint(exporters[i], endpoints[i]));
    }
}

void Configuration::set_tracing_collectors(Configuration::CollectorSet collectors)
{
    tracing_collectors = collectors;
}

void Configuration::set_logging_collectors(const std::string &exporters_str,
                                           const std::string &endpoints_str,
                                           const std::string &source)
{
    std::vector<Exporter> exporters;
    std::vector<std::string> endpoints;

    logging_collectors.clear();
    convert_exporter_and_endpoint_strings(exporters_str, endpoints_str, source, exporters, endpoints);

    if(exporters.size() != endpoints.size())
    {
        endpoints.reserve(exporters.size());
        std::transform(exporters.begin(), exporters.end(), back_inserter(endpoints), get_logging_default_endpoint);
    }
    for(unsigned int i = 0; i < exporters.size(); ++i)
    {
        validate_exporter_and_endpoint(exporters[i], endpoints[i]);
        logging_collectors.emplace_back(exporters[i], endpoints[i]);
    }
}

void Configuration::set_logging_collectors(Configuration::CollectorSet collectors)
{
    logging_collectors = collectors;
}

void Configuration::set_tracing_collectors_from_env(const char *exporter_env_var, const char *endpoint_env_var)
{
    std::string exporter_env_str;
    std::string endpoint_env_str;

    ApiUtil::instance()->get_env_var(exporter_env_var, exporter_env_str);
    ApiUtil::instance()->get_env_var(endpoint_env_var, endpoint_env_str);

    set_tracing_collectors(exporter_env_str, endpoint_env_str, "environment variables");
}

void Configuration::set_logging_collectors_from_env(const char *exporter_env_var, const char *endpoint_env_var)
{
    std::string exporter_env_str;
    std::string endpoint_env_str;

    ApiUtil::instance()->get_env_var(exporter_env_var, exporter_env_str);
    ApiUtil::instance()->get_env_var(endpoint_env_var, endpoint_env_str);

    set_logging_collectors(exporter_env_str, endpoint_env_str, "environment variables");
}

/// @throw Tango::API_InvalidArgs
void Configuration::convert_exporter_and_endpoint_strings(const std::string &exporter_str,
                                                          const std::string &endpoint_str,
                                                          const std::string &source,
                                                          std::vector<Exporter> &exporters,
                                                          std::vector<std::string> &endpoints) const
{
    if(exporter_str != "")
    {
        for(auto const &exp : split_str_var(exporter_str))
        {
            exporters.push_back(to_exporter(detail::to_lower(exp)));
        }
    }
    else
    {
        exporters.push_back(kDefaultExporter);
    }

    if(endpoint_str != "")
    {
        endpoints = split_str_var(endpoint_str);
    }

    // If _any_ endpoints are set then all must be set
    if(exporters.size() != endpoints.size() && endpoints.size() > 0)
    {
        std::stringstream sstr;
        sstr << "Problem setting telemetry endpoints from " << source << ": ";
        sstr << "The number of endpoints (" << endpoints.size() << ") must match";
        sstr << " the number of exporters (" << exporters.size() << ")";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, sstr.str());
    }
}

std::string Configuration::get_tracing_default_endpoint(Exporter exporter_type)
{
    std::string endpoint;
    switch(exporter_type)
    {
    case Exporter::grpc:
        endpoint = Configuration::DEFAULT_GRPC_TRACES_ENDPOINT;
        break;
    case Exporter::http:
        endpoint = Configuration::DEFAULT_HTTP_TRACES_ENDPOINT;
        break;
    case Exporter::console:
        endpoint = Configuration::DEFAULT_CONSOLE_TRACES_ENDPOINT;
        break;
    default:
        TANGO_ASSERT_ON_DEFAULT(exporter_type);
    }
    return endpoint;
}

std::string Configuration::get_logging_default_endpoint(Exporter exporter_type)
{
    std::string endpoint;
    switch(exporter_type)
    {
    case Exporter::grpc:
        endpoint = Configuration::DEFAULT_GRPC_LOGS_ENDPOINT;
        break;
    case Exporter::http:
        endpoint = Configuration::DEFAULT_HTTP_LOGS_ENDPOINT;
        break;
    case Exporter::console:
        endpoint = Configuration::DEFAULT_CONSOLE_LOGS_ENDPOINT;
        break;
    default:
        TANGO_ASSERT_ON_DEFAULT(exporter_type);
    }
    return endpoint;
}

void Configuration::validate_exporter_and_endpoint(Exporter exporter, const std::string &endpoint)
{
    switch(exporter)
    {
    case Exporter::grpc:
#ifndef TANGO_TELEMETRY_USE_GRPC
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs,
                              "Requested grpc trace exporter, but compiled without GRPC support.");
#else
        if(!Configuration::is_valid_grpc_endpoint(endpoint))
        {
            std::stringstream err;
            err << "the specified telemetry endpoint '" << endpoint << "' is invalid - ";
            err << "expecting a valid gRPC endpoint - e.g., grpc://localhost:4318";
            TANGO_LOG << err.str() << std::endl;
            TANGO_THROW_EXCEPTION(API_InvalidArgs, err.str());
        }
        break;
#endif
    case Exporter::http:
#ifndef TANGO_TELEMETRY_USE_HTTP
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs,
                              "Requested http trace exporter, but compiled without HTTP support.");
#else
        if(!Configuration::is_valid_http_endpoint(endpoint))
        {
            std::stringstream err;
            err << "the specified telemetry endpoint '" << endpoint << "' is invalid - ";
            err << "expecting a valid http[s]:// url - e.g., http://localhost:4317/v1/traces";
            TANGO_LOG << err.str() << std::endl;
            TANGO_THROW_EXCEPTION(API_InvalidArgs, err.str());
        }
        break;
#endif
    case Exporter::console:
        if(!Configuration::is_valid_console_endpoint(endpoint))
        {
            std::stringstream err;
            err << "the specified telemetry endpoint '" << endpoint << "' is invalid - ";
            err << R"(expecting "cout" or "cerr")";
            TANGO_LOG << err.str() << std::endl;
            TANGO_THROW_EXCEPTION(API_InvalidArgs, err.str());
        }
        break;
    default:
        TANGO_ASSERT_ON_DEFAULT(exporter);
    }
}

Configuration::CollectorSet Configuration::get_tracing_collectors() const
{
    return tracing_collectors;
}

Configuration::CollectorSet Configuration::get_logging_collectors() const
{
    return logging_collectors;
}

bool Configuration::remove_collector(Configuration::CollectorSet &collectorSet,
                                     const Configuration::Collector &collector)
{
    bool found{false};
    for(auto iter = collectorSet.begin(); iter != collectorSet.end();)
    {
        if(*iter == collector)
        {
            found = true;
            iter = collectorSet.erase(iter);
        }
        else
        {
            ++iter;
        }
    }
    return found;
}

bool Configuration::remove_tracing_collector(const Configuration::Collector &collector)
{
    return remove_collector(tracing_collectors, collector);
}

bool Configuration::remove_logging_collector(const Configuration::Collector &collector)
{
    return remove_collector(logging_collectors, collector);
}

bool Configuration::add_collector(Configuration::CollectorSet &collectorSet, Configuration::Collector collector)
{
    bool added{false};
    if(std::find(collectorSet.begin(), collectorSet.end(), collector) == collectorSet.end())
    {
        added = true;
        collectorSet.push_back(collector);
    }
    return added;
}

bool Configuration::add_tracing_collector(Configuration::Collector collector)
{
    return add_collector(tracing_collectors, collector);
}

bool Configuration::add_logging_collector(Configuration::Collector collector)
{
    return add_collector(logging_collectors, collector);
}

} // namespace Tango::telemetry
