/*
 * SPDX-FileCopyrightText: 2025 Copyright contributors to the cppTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */
#ifndef TANGO_COMMON_TELEMETRY_INTERFACE_H
#define TANGO_COMMON_TELEMETRY_INTERFACE_H

#include <tango/common/defines.h>

#ifdef TANGO_USE_TELEMETRY

  #include <tango/common/telemetry/Configuration.h>
  #include <tango/common/telemetry/Scope.h>
  #include <tango/server/Logging.h>

  #include <sstream>
  #include <thread>

namespace Tango::telemetry
{

class InterfaceImplementation;

//---------------------------------------------------------------------------------------------------------------------
// InterfacePtr
//---------------------------------------------------------------------------------------------------------------------
class Interface;
using InterfacePtr = std::shared_ptr<Interface>;

//---------------------------------------------------------------------------------------------------------------------
// The telemetry interface currently attached to the current thread (thread_local storage) - see Interface for details
//---------------------------------------------------------------------------------------------------------------------
// This is notably set by the Tango device upon reception of an external request using the RAII idiom implemented by
// the InterfaceScope class (which makes a call to Interface::set_current). We do attach the telemetry interface of the
// device to the thread handling the client request at the most upstream point in the call stack - i.e. the location
// considered as the entry point of the request. This allows any downstream code to generate traces on behalf of a
// given device by retrieving the "current" telemetry interface. This contributes to the required "per device telemetry"
// design. A pure client will tipically instantiate a client flavor of the Interface class at startup. This interface
// can be shared and activated by any thread wanting to generate telemetry signals on behalf og the client.
//---------------------------------------------------------------------------------------------------------------------
extern thread_local InterfacePtr current_telemetry_interface;

//---------------------------------------------------------------------------------------------------------------------
// Interface: an interface for Tango telemetry
//---------------------------------------------------------------------------------------------------------------------
//! Any "entity" wanting to generate telemetry signals on its own (i.e. not on behalf of another entity) should hold its
//! own instance of the Tango::telemetry::Interface class. That's the case for any Tango device (per device telemetry
//! design). The same rule applies to a pure client. The interface configuration allows to distinguish the two cases.
//! See the thread_local Tango::telemetry::current_telemetry_interface variable for details.
//---------------------------------------------------------------------------------------------------------------------
class Interface
{
    //-----------------------------------------------------------------------------------------------------------------
    // internal helper function: thread_id_to_string (for tracing location)
    //-----------------------------------------------------------------------------------------------------------------
    static std::string thread_id_to_string() noexcept
    {
        std::stringstream ss;
        ss << std::this_thread::get_id();
        return ss.str();
    }

    //-----------------------------------------------------------------------------------------------------------------
    // internal helper function: forward declaration of the method returning the default interface.
    // see Interface::get_current for details regarding the semantic and usage of the default interface.
    //-----------------------------------------------------------------------------------------------------------------
    static Tango::telemetry::InterfacePtr get_default_interface();

  public:
    //-----------------------------------------------------------------------------------------------------------------
    //  CTORs & ASSIGNMENT OPERATORs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! Construct a new Interface object.
    //!
    //! This constructor might throw a Tango::DevFailed in case the instantiation of the underlying interface failed -
    //! it will notably be the case if one of telemetry endpoints is ill-formed.
    //!
    //! @param cfg the v configuration.
    //!
    //! \throws Tango::DevFailed (see description)
    //!
    //! \see Configuration
    //-----------------------------------------------------------------------------------------------------------------
    Interface(const Configuration &cfg);

    //- some disabled features
    Interface(const Interface &) = delete;
    Interface(Interface &&) = delete;
    Interface &operator=(const Interface &) = delete;
    Interface &operator=(Interface &&) = delete;

    ~Interface();

    //-----------------------------------------------------------------------------------------------------------------
    //  MISC. ACCESSORs & MUTATORs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! Check if the interface is the default one.
    //!
    //! \returns: true if the telemetry interface the default one, returns false otherwise.
    //-----------------------------------------------------------------------------------------------------------------
    bool is_default() const noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //  MISC. ACCESSORs & MUTATORs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! Get the interface state.
    //!
    //! A disabled Interface does nothing (telemetry signals are dropped).
    //!
    //! \returns: true if the telemetry interface is enabled, returns false otherwise.
    //-----------------------------------------------------------------------------------------------------------------
    bool is_enabled() const noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Enable the interface.
    //!
    //! A disabled Interface does nothing (i.e., telemetry signals are silently dropped).
    //-----------------------------------------------------------------------------------------------------------------
    void enable() noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Disable the interface.
    //!
    //! A disabled Interface does nothing (i.e., telemetry signals are silently dropped).
    //-----------------------------------------------------------------------------------------------------------------
    void disable() noexcept;

    //! set topics
    void set_topics(const std::vector<std::string> &topics);
    void set_topics(const std::string &topics);

    //! get topics
    std::vector<std::string> get_topics();

    //! Enable or disable tracing
    void set_tracing(bool enable_flag) noexcept;

    //! Test if tracing is enabled
    bool is_tracing_enabled() noexcept;

    //! Update config and stop/start the interface
    void update_tracing_configuration(Configuration newcfg) noexcept;

    //! Update config but DO NOT stop/start the interface (for logging!)
    void update_configuration(Configuration newcfg) noexcept;

    //! Test if logging is enabled
    bool is_logging_enabled() noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Get the interface identifier.
    //!
    //! The interface identifier is a user defined label that do not play any particular role in the behavior of the
    //! telemetry interface. It is specified at instantiation and can't be changed.
    //!
    //! \returns The interface identifier.
    //!
    //! \see Configuration
    //-----------------------------------------------------------------------------------------------------------------
    const std::string &get_id() const noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Get the interface configuration.
    //!
    //! \returns The interface configuration.
    //!
    //! \see Configuration
    //-----------------------------------------------------------------------------------------------------------------
    const Configuration &get_configuration() const noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    // API RELATED TO SPANs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! Start a new Span.
    //!
    //! It is recommended to use the TANGO_TELEMETRY_SPAN macro rather than this function. Please use the macro unless
    //! you know what you're doing. A misusage of the \p kind parameter can notably created some side effects on backend
    //! side.
    //
    //! For consistency reasons, it is recommended to use the TELEMETRY_CURRENT_FUNCTION macro to name the new span.
    //! Some attributes can be attached to the new span. Use \ref Span::set_attribute to add an individual attribute
    //! to the span.
    //!
    //! In case of memory error, an invalid default span is returned. The latter will simply be transparently
    //! dropped by the underlying implementation. This is part of our "transparent telemetry strategy" aiming to
    //! guarantee that the telemetry service never influences the behavior of the client or server making use of it.
    //!
    //! Example:
    //! @code
    //! auto span = TANGO_TELEMETRY_SPAN(TELEMETRY_CURRENT_FUNCTION, {{"key", "value"}});
    //! auto scope = Tango::telemetry::Interface::Scope(span);
    //! @endcode
    //!
    //! @param name the name of the Span - use TELEMETRY_CURRENT_FUNCTION unless you want to specify a particular name.
    //! @param attributes a std::map of key/value pairs where the key is a std::string and the value an
    //! AttributeValue.
    //! @param kind the Span::Kind - defaults to Span::Kind::kInternal.
    //!
    //! \returns std::shared_ptr holding the new Tango::telemetry::Span or an invalid default span.
    //!
    //! \see TANGO_TELEMETRY_SPAN
    //! \see Tango::telemetry::Span::set_attribute
    //! \see Tango::telemetry::AttributeValue
    //-----------------------------------------------------------------------------------------------------------------
    SpanPtr start_span(const std::string &name,
                       const Attributes &attributes = {},
                       const Span::Kind &kind = Span::Kind::kInternal) noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Return the current Span.
    //!
    //! Allows to retrieve the current span for a downstream context. The TANGO_TELEMETRY_ADD_ATTRIBUTE macro offers a
    //! syntactic shortcut for this function.
    //!
    //! @code
    //! // typical usage
    //! auto current_span = Tango::telemetry::Interface::get_current().get_current_span();
    //! current_span.add_event("some annotation");
    //! @endcode
    //!
    //! \returns A SpanPtr (i.e., a std::shared_ptr) holding the current Tango::telemetry::Span.
    //-----------------------------------------------------------------------------------------------------------------
    SpanPtr get_current_span() const noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    // API RELATED TO SCOPEs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! A helper function for macros passing __FILE__ and __LINE__. It's not supposed to be used directly.
    //! The aim of this function is to make the specified Span the active one for the lifetime of the returned Scope.
    //!
    //! Example:
    //! @code
    //! auto span = TANGO_TELEMETRY_SPAN(TELEMETRY_CURRENT_FUNCTION);
    //! auto scope = TANGO_TELEMETRY_SCOPE(span);
    //! @endcode
    //!
    //! @param span the Span to activate.
    //!
    //! \returns a Tango::telemetry::Scope.
    //!
    //! \see TANGO_TELEMETRY_SCOPE
    //! \see Tango::telemetry::Scope
    //-----------------------------------------------------------------------------------------------------------------
    Tango::telemetry::Scope
        scope(Tango::telemetry::SpanPtr &span, const char *file = "unknown-source-file", int line = -1)
    {
        if(!span)
        {
            return Tango::telemetry::Scope();
        }
        span->set_attribute("code.filepath", Tango::logging_detail::basename(file));
        span->set_attribute("code.lineno", std::to_string(line));
        span->set_attribute("thread.id", thread_id_to_string());
        return Tango::telemetry::Scope(span);
    }

    //-----------------------------------------------------------------------------------------------------------------
    // API RELATED TO THE ACTIVE INTERFACE
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! Get the active Tango::telemetry::Interface for the current thread.
    //!
    //! The current interface is either the one activated by a pure client at startup or the one activated by the cpp
    //! kernel at the entry point of a remote request. In case there is no activate interface for the current thread,
    //! the default one is returned. The aim of the default interface is to guarantee that we always have a valid
    //! interface to return to the caller. However, the default interface will silently drop any telemetry signal.
    //! Having a default "noop" interface simplifies the implementation by avoid systematic validity checking of the
    //! Tango::telemetry::current_telemetry_interface.
    //!
    //! \returns: the active Tango::telemetry::Interface (as a std::shared_ptr) or the default one if none.
    //!
    //! \see Tango::telemetry::Interface::set_current
    //-----------------------------------------------------------------------------------------------------------------
    static InterfacePtr get_current()
    {
        return Tango::telemetry::current_telemetry_interface ? Tango::telemetry::current_telemetry_interface
                                                             : Interface::get_default_interface();
    }

    //-----------------------------------------------------------------------------------------------------------------
    //! Set the active Tango::telemetry::Interface for the current thread.
    //!
    //! @param telemetry_interface the Tango::telemetry::Interface to activate.
    //!
    //! \see Tango::telemetry::Interface::get_current
    //-----------------------------------------------------------------------------------------------------------------
    static void set_current(Tango::telemetry::InterfacePtr &telemetry_interface) noexcept
    {
        Tango::telemetry::current_telemetry_interface = telemetry_interface;
    }

    //-----------------------------------------------------------------------------------------------------------------
    // API RELATED TO CONTEXT PROPAGATION ALONG THE DISTRIBUTED CALL STACK
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! Set the trace context (part of the trace context propagation)
    //!
    //! This method is used by the Tango::Device_[X]Impl to setup the trace context upon reception of a remote call.
    //! This flavor of "set_trace_context" is used where the caller is using an IDL version >= 4 and propagates context
    //! information. A new span is created and becomes the active one thanks to the returned Scope. The name of the new
    //! span is specified by the "new_span_name" argument.
    //!
    //! @param new_span_name the name of the Span created behind the scenes.
    //! @param span_attrs some optional Span attributes - just pass {} if none.
    //! @param client_identification the data structure containing the telemetry context coming from the client.
    //!
    //! \see Tango::telemetry::Interface::get_trace_context
    //-----------------------------------------------------------------------------------------------------------------
    static ScopePtr set_trace_context(const std::string &new_span_name,
                                      Tango::telemetry::Attributes span_attrs,
                                      const Tango::ClntIdent &client_identification,
                                      const char *client_ip) noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Set the trace context (part of the trace context propagation)
    //!
    //! This method is used by the Tango::Device_[X]Impl to setup the trace context upon reception of a remote call.
    //! This flavor of "set_trace_context" is used where the caller is using an IDL version < 4 and doesn't propagate
    //! any context information (local tracing only). A new span is created and becomes the active one thanks to the
    //! returned Scope. The name of the new span is specified by the "new_span_name" argument.
    //!
    //! @param new_span_name the name of the Span created behind the scenes.
    //! @param span_attrs some optional Span attributes.
    //!
    //! \see Tango::telemetry::Interface::get_trace_context
    //-----------------------------------------------------------------------------------------------------------------
    static ScopePtr set_trace_context(const std::string &new_span_name,
                                      const Tango::telemetry::Attributes &span_attrs = {}) noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Set the trace context (part of the trace context propagation)
    //!
    //! This is a helper function for pyTango or any other binding. It's not supposed to be used directly.
    //!
    //! All we have to do here, is to provide the binding with a way to get and set the current telemetry context when
    //! it calls the kernel (e.g., making use of a DeviceProxy). This method allows to set the current telemetry context
    //! (i.e., the one currently active on binding side) using its W3C format (i.e., trough the trace_parent &
    //! trace_state headers passed as arguments.
    //!
    //! The new context creates a span that becomes the active one thanks to the returned Scope. The name of that new
    //! span is specified by the "new_span_name" argument.
    //!
    //! @param new_span_name the name of the Span created behind the scenes.
    //! @param trace_parent the W3C 'traceparent' header.
    //! @param trace_state the W3C 'tracestate' header.
    //! @param kind the 'kind' of Span to create.
    //!
    //! \see Tango::telemetry::Interface::get_trace_context
    //! \see Tango::telemetry::Span::Kind
    //-----------------------------------------------------------------------------------------------------------------
    static ScopePtr set_trace_context(const std::string &new_span_name,
                                      const std::string &trace_parent,
                                      const std::string &trace_state,
                                      const Span::Kind &kind = Span::Kind::kClient) noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //! Get the current trace context.
    //!
    //! This method is used by the Tango::Connection (and its child classes - e.g., DeviceProxy) to propagate the trace
    //! context to the callee. It could also be used by pyTango. The trace context is obtained in its W3C format trough
    //! the two strings passed as arguments(trace_parent & trace_state). It is also use by any binding (e.g., pyTango)
    //! that wants to propagate the c++ tracing context in its own environment.
    //!
    //! @param trace_parent the W3C 'traceparent' header.
    //! @param trace_state the W3C 'tracestate' header.
    //!
    //! \see Tango::telemetry::Interface::set_trace_context
    //-----------------------------------------------------------------------------------------------------------------
    static void get_trace_context(std::string &trace_parent, std::string &trace_state) noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    // API RELATED TO LOGs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    // Instantiate a log4tango::Appender routing the device logs to the telemetry collector.
    //
    // The caller obtains the ownership of the returned pointer and must delete once done with it.
    //-----------------------------------------------------------------------------------------------------------------
    log4tango::Appender *get_appender() const noexcept;

    //-----------------------------------------------------------------------------------------------------------------
    //  MISC HELPERs
    //-----------------------------------------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------------------------------------
    //! A helper function that tries to extract an error message from the current exception.
    //!
    //! Example:
    //! @code
    //! try
    //! {
    //!   ...
    //! }
    //! catch (...)
    //! {
    //!   auto error_msg = Tango::telemetry::extract_exception_info();
    //!   std::cout << "oops, an error occurred: " << error_msg << std::endl;
    //! }
    //! @endcode
    //!
    //! @param current_exception the exception from which we want to extract the message. Defaults to the current one.
    //!
    //! \returns the error message
    //-----------------------------------------------------------------------------------------------------------------
    static std::string extract_exception_info(std::exception_ptr c = std::current_exception());

    //-----------------------------------------------------------------------------------------------------------------
    //! A helper function that tries to extract both the error type and a message from the current exception.
    //!
    //! Example:
    //! @code
    //! try
    //! {
    //!   ...
    //! }
    //! catch (...)
    //! {
    //!   std::string error_type, error_msg;
    //!   Tango::telemetry::extract_exception_info(error_type, error_msg);
    //!   std::cout << "oops, an error of type " << error_type << " has been caught: " << error_msg << std::endl;
    //! }
    //! @endcode
    //!
    //! @param error_type the exception type as a std::string.
    //! @param error_message the associated error message as a std::string.
    //-----------------------------------------------------------------------------------------------------------------
    static void extract_exception_info(std::string &error_type, std::string &error_message);

  private:
    // opaque implementation
    std::shared_ptr<InterfaceImplementation> impl;
};

} // namespace Tango::telemetry

#endif // TANGO_USE_TELEMETRY

#endif // TANGO_COMMON_TELEMETRY_INTERFACE_H
