#pragma once

#include "felix/felix_client_thread_extension423.hpp"

#include <any>
#include <chrono>
#include <cstdint>
#include <format>
#include <map>
#include <ostream>
#include <set>
#include <span>
#include <sstream>
#include <string>
#include <variant>
#include <vector>

using BitFieldRecordValue = std::variant<uint64_t, int64_t, double, bool, std::string>;

namespace felix {

  class BitFieldRecord {
    public:
      explicit BitFieldRecord(const std::any& data);
      [[nodiscard]] const std::string& get_decoded_value() const;
      [[nodiscard]] const std::string& get_unit() const;
      [[nodiscard]] std::uint64_t get_raw_value() const;
      [[nodiscard]] const BitFieldRecordValue& get_value() const;

    private:
      std::string decoded_value;
      BitFieldRecordValue value;
      std::string unit;
      std::uint64_t raw_value{};
  };

  struct DeviceId {
    std::string ip;
    std::uint16_t port{};
    std::uint64_t device_no{};

    constexpr auto operator<=>(const DeviceId&) const = default;
  };
}

template <>
struct std::formatter<felix::BitFieldRecord> {
    constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

    auto format(const felix::BitFieldRecord& bitfield, std::format_context& ctx) const {
        const auto value_formatted =
            std::visit([](auto&& value) { return std::format("{}", value); },
            bitfield.get_value());
        return std::format_to(ctx.out(),
                    "BitFieldRecord decoded: '{}', unit: '{}', "
                    "raw_value: {:#x} {}, value: {}",
                    bitfield.get_decoded_value(), bitfield.get_unit(),
                    bitfield.get_raw_value(), bitfield.get_raw_value(),
                    value_formatted);
    }
};

template <>
struct std::formatter<felix::DeviceId> {
    constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

    auto format(const felix::DeviceId& dev_id, std::format_context& ctx) const {
        return std::format_to(ctx.out(),
                    "{}:{}:{}",
                    dev_id.ip, dev_id.port, dev_id.device_no);
    }
};

class FelixClientThreadExtension520 : public FelixClientThreadExtension423 {

public:
    /**
     * @brief Data callback executed by the event loop (network reactor) thread.
     * @attention Blocking in this function blocks the eventloop, return as soon as possible.
     *
     * @param fid The fid (e-link identifier) from which the message originates
     * @param data The data
     * @param status One-byte field used to report errors in the message, see felix_client_status.h
     */
    typedef std::function<void (uint64_t fid, std::span<const uint8_t> data, uint8_t status)> OnDataCallbackV2;

    /**
     * @brief Data callback executed by the event loop (network reactor) thread returning entire buffer.
     * @attention Blocking in this function blocks the eventloop, return as soon as possible.
     *
     * @param data The data of the entire network buffer (undecoded)
     */
    typedef std::function<void (std::span<const uint8_t> data)> OnBufferCallback;

    /**
     * @brief Callback reporting that a connection has been refused.
     * @param fid The fid for which the connection is refused.
     */
    typedef std::function<void (uint64_t fid)> OnConnectionRefusedCallback;

    /**
     * @brief Callback reporting that a set of subscriptions has been lost.
     * @param fid The fids for which the connection is down.
     */
    typedef std::function<void (const std::set<std::uint64_t>& fids)> OnSubscriptionsLostCallback;

    /**
     * @brief Callback reporting that a link is resubscribed (after being lost).
     * @param fid The fid for which the connection is re-established.
     */
    typedef std::function<void (std::uint64_t fid)> OnResubscriptionCallback;

    /**
     * Data structure that includes both configuration parameters
     * and pointers to callbacks using new on data callback.
     */
    struct ConfigV2 {
        OnDataCallbackV2 on_data_callback{nullptr};
        OnBufferCallback on_buffer_callback{nullptr};
        OnInitCallback on_init_callback{nullptr};
        OnConnectCallback on_connect_callback{nullptr};
        OnDisconnectCallback on_disconnect_callback{nullptr};
        OnConnectionRefusedCallback on_refused_callback{nullptr};
        OnSubscriptionsLostCallback on_subscriptions_lost_callback{nullptr};
        OnResubscriptionCallback on_resubscription_callback{nullptr};
        Properties property{};
    };

    /**
     * @brief Statistics for the block decoder
     *
     * This structure contains statistics about the block decoder, such as the number of processed
     * blocks, dropped blocks, invalid data blocks, invalid header blocks, out-of-sequence blocks,
     * and corrupted streams.
     */
    struct BlockDecoderStats {
      std::uint64_t processed_blocks{};       //!< Number of processed blocks
      std::uint64_t dropped_blocks{};         //!< Number of dropped blocks
      std::uint64_t invalid_data_blocks{};    //!< Number of blocks with invalid data
      std::uint64_t invalid_header_blocks{};  //!< Number of blocks with invalid header
      std::uint64_t oos_blocks{};             //!< Number of blocks with out-of-sequence data
      std::uint64_t corrupted_streams{};      //!< Number of chunks with corrupted streams
    };

    /**
     * @brief Asynchronously initialize a send connection to FELIX
     *
     * This function checks if a connection has to be established and opens a new
     * connection if needed. The function will return immediately. The result is
     * communicated through the on_connect callback. If the connection is refused,
     * the on_refused callback is called. If already the connection request fails,
     * an exception is thrown.
     *
     * @throws felix::BusException if the tag is not found in the bus
     * @param fid The tag to send data to
     */
    virtual void init_send_data_nb(std::uint64_t fid) = 0;
    using FelixClientThreadExtension423::init_send_data;

    /**
     * @brief Initialize a send connection to FELIX
     *
     * This function checks if a connection has to be established and opens a
     * new connection if needed. The function will block until the connection is
     * established or the timeout is reached. If the function returns without
     * throwing an exception, the connection is established.
     *
     * @throws felix::SendConnectionTimeoutException if the connection was
     * not established within the timeout
     * @throws felix::SendConnectionRefusedException if the connection was
     * refused
     * @throws felix::BusException if the tag is not found in the bus
     * @param fid The tag to send data to
     * @param timeout The timeout for establishing the connection
     */
    virtual void init_send_data(std::uint64_t fid,
                                std::chrono::milliseconds timeout) = 0;
    using FelixClientThreadExtension423::send_data;

    /**
     * @brief Function to send a message to a remote fid
     *
     * This function sends a single message to FELIX. If a buffered sender is used (depending on
     * information in the bus), the flush parameter can be used to flush the buffer and send the
     * message immediately.
     *
     * @note Can be called without init_send_data but this behavior will change and throw an
     * exception in the future.
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     * @param fid The tag to send data to
     * @param data The data to send
     * @param flush Flush the buffer
     */
    virtual void send_data(std::uint64_t fid, std::span<const std::uint8_t> data, bool flush) = 0;

    /**
     * @brief Function to send multiple messages to a remote fid
     *
     *
     * This function sends multiple messages to FELIX. If a buffered sender is used (depending on
     * information in the bus) it is the same as calling send_data for each message. For unbuffered
     * sending, messages will still be send together in one transaction (or as slittle as possible
     * to fit into buffers).
     *
     * @note Can be called without init_send_data but this behavior will change and throw an
     * exception in the future.
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     * @param fid The tag to send data to
     * @param data The data to send
     * @param flush Flush the buffer
     */
    virtual void send_data(std::uint64_t fid, const std::vector<std::span<const std::uint8_t>> &msgs) = 0;
    using FelixClientThreadExtension423::subscribe;

    /**
     * @brief Synchronously subscribe to an fid
     *
     * The remote endpoint is identified from the felix-bus and a connection is
     * established if not already present. The function will block until the
     * link is subscribed or the timeout is reached. If the function returns
     * without throwing an exception, the connection is established. In case the
     * subscription fails the exception will contain more information about the
     * cause.
     *
     * @throws felix::BusException If the information is not available in
     * the bus
     * @throws felix::FailedSubscribeException If the subscription fails
     * @throws felix::AlreadySubscribedException If the link is already
     * subscribed
     * @param fid The fid to subscribe to
     * @param timeout The timeout for establishing the connection
     */
    virtual void subscribe(std::uint64_t fid, std::chrono::milliseconds timeout) = 0;

    /**
     * @brief Synchronously subscribe to a vector of fids
     *
     * The remote endpoints are identified from the felix-bus and connections
     * are established if not already present. The function will block until the
     * links are subscribed or the timeout is reached. If the function returns
     * without throwing an exception, the connections are established. In case
     * the subscription fails the exception will contain more information about
     * the cause.
     *
     * @throws felix::BusException If the information is not available in
     * the bus
     * @throws felix::FailedSubscribeException If the subscription to any
     * link fails
     * @throws felix::AlreadySubscribedException If all links are already
     * subscribed
     * @param fids The fids to subscribe to
     * @param timeout The timeout for establishing the connection
     */
    virtual void subscribe(const std::vector<uint64_t> &fids, std::chrono::milliseconds timeout) = 0;

    /**
     * @brief Synchronously subscribe to an fid
     *
     * The remote endpoint is identified from the felix-bus and a connection is
     * established if not already present. The function will return
     * immediately. A success is communicated through the on_connect callback.
     * If the subscription request fails, an exception is thrown. Other failures
     * are not communicated (but the on_connect callback will not be called).
     *
     * @throws felix::BusException If the information is not available in
     * the bus
     * @throws felix::FailedAsyncSubscribeException If the subscription fails
     * @throws felix::AlreadySubscribedException If the link is already
     * subscribed
     * @param fid The fid to subscribe to
     * @param timeout The timeout for establishing the connection
     */
    virtual void subscribe_nb(std::uint64_t fid) = 0;

    /**
     * @brief Aynchronously subscribe to a vector of fids
     *
     * The remote endpoints are identified from the felix-bus and connections
     * are established if not already present. The function will return
     * immediately. A success is communicated through the on_connect callback.
     * If the subscription request fails, an exception is thrown. Other failures
     * are not communicated (but the on_connect callback will not be called).
     *
     * @throws felix::BusException If the information is not available in
     * the bus
     * @throws felix::FailedAsyncSubscribeException If the subscription
     * fails
     * @throws felix::AlreadySubscribedException If all links is already
     * subscribed
     * @param fids The fids to subscribe to
     * @param timeout The timeout for establishing the connection
     */
    virtual void subscribe_nb(const std::vector<uint64_t> &fids) = 0;
    using FelixClientThreadExtension423::unsubscribe;

    /**
     * @brief Synchronously unsubscribe from a tag
     *
     * This function unsubscribes from a tag. The function will block until the link is unsubscribed
     * or the timeout is reached. If the function returns without throwing an exception, the
     * link is unsubscribed. In case the unsubscription fails the exception will contain more
     * information about the cause.
     *
     * @throws felix::NotSubscribedException If the FID is not subscribed
     * @throws felix::FailedUnsubscribeException If the unsubscription fails
     * @throws felix::UnsubscribeTimeoutException If the unsubscription times out
     * @param fid The tag to unsubscribe from
     * @param timeout The timeout for unsubscribing
     */
    virtual void unsubscribe(std::uint64_t fid, std::chrono::milliseconds timeout) = 0;

    /**
     * @brief Asynchronously unsubscribe from a tag
     *
     * This function unsubscribes from a tag. The function will return immediately. A success is
     * communicated through the on_disconnect callback. If the unsubscription request fails, an
     * exception is thrown. Other failures are not communicated (but the on_disconnect callback will
     * not be called).
     *
     * @throws felix::NotSubscribedException If the FID is not subscribed
     * @throws felix::FailedUnsubscribeException If the unsubscription fails
     * @param fid The tag to unsubscribe from
     */
    virtual void unsubscribe_nb(std::uint64_t fid) = 0;
    using FelixClientThreadExtension423::user_timer_start;

    /**
     * @brief This function creates a new blank trickle configuration
     */
    virtual void create_trickle_config() = 0;

    /**
     * @brief Adds data to the current trickle configuration
     *
     * @throws felix::TrickleLogicException if called before create_trickle_config()
     *
     * @param elink The fid of the FE device
     * @param payload The binary payload data
     */
    virtual void append_trickle_config(const std::uint64_t fid, const std::vector<uint8_t>& payload) = 0;

    /**
     * @brief Send trickle configuration to a FELIX device
     *
     * @throws felix::TrickleLogicException if called before append_trickle_config()
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     *
     * @param fid The target FELIX device ID
     * @param data The binary payload for the trickle configuration
     */
    virtual void send_trickle_config(std::uint64_t fid) = 0;

    /**
     * @brief Start trickle sending on a FELIX device
     *
     * This function sends a command to start trickle sending on a FELIX device.
     *
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     *
     * @param fid The target FELIX device ID
     */
    virtual void start_trickle(std::uint64_t fid) = 0;

    /**
     * @brief Stop trickle sending on a FELIX device
     *
     * This function sends a command to stop trickle sending on a FELIX device.
     *
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     *
     * @param fid The target FELIX device ID
     */
    virtual void stop_trickle(std::uint64_t fid) = 0;

    /**
     * @brief Configure BCID range for trickle sending
     *
     * This function sends a command to configure the BCID range for trickle sending.
     * DISCALIMER: it has not yet been implemented.
     *
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     *
     * @param fid The target FELIX device ID
     * @param first_bcid First BCID in the range
     * @param last_bcid Last BCID in the range
     */
    virtual void select_trickle_bcids(std::uint64_t fid, std::uint32_t first_bcid, std::uint32_t last_bcid) = 0;

    /**
     * @brief Configure throttling factor for trickle sending
     *
     * This function sends a command to configure the throttling factor that slows firmware trickling.
     * DISCLAIMER: it has not yet been implemented.
     *
     * @throws felix::SendWhileConnectionDownException if the connection was lost and is being re-established
     * @throws felix::ResourceNotAvailableException if no network buffers were available (try again)
     * @throws felix::MessageTooBigException if message was too big (retrying will fail again)
     * @throws felix::BusException if the tag is not found in the bus
     *
     * @param fid The target FELIX device ID
     * @param throttle_factor slows trickling by N times [1 = no throttle]
     */
    virtual void throttle_trickle(std::uint64_t fid, std::uint32_t throttle_factor) = 0;

    /**
     * @brief Start user timer function in the eventloop thread
     *
     * @param interval interval time
     */
    virtual void user_timer_start(std::chrono::milliseconds interval) = 0;

    /**
     * @brief Get the number of available buffers of all senders per FID
     *
     * Returns the minimum number of buffers since the last call to this function per FID.
     *
     * @return Number of buffers per FID for all senders
     */
    [[nodiscard]] virtual std::map<std::uint64_t, std::size_t> get_available_send_buffers() = 0;

    /**
     * @brief Get the statistics for the block decoder
     *
     * Returns the statistics for the block decoder, such as the number of processed blocks,
     * dropped blocks, invalid data blocks, invalid header blocks, out-of-sequence blocks,
     * and corrupted streams if block decoding is used.
     *
     * @return Block decoder statistics
     */
    [[nodiscard]] virtual std::map<std::uint64_t, BlockDecoderStats> get_block_decoder_stats() const = 0;

    /**
     * @brief Reads register, i2c register, alias or group
     *
     * @throws felix::FelixIoException if any felix_io error occurred
     *
     * @param dev_id device id pointing to the device to be read
     * @param name register, i2c, alias or group name
     *
     * @return map of list of bitfields, accessible by name
     */
    [[nodiscard]] virtual std::map<std::string, std::vector<felix::BitFieldRecord>> read_register(const felix::DeviceId& dev_id, const std::string& name) const = 0;

    /**
     * @brief Writes a register
     *
     * @throws felix::FelixIoException if any felix_io error occurred
     *
     * @param dev_id device id pointing to the device to be written
     * @param name register name
     * @param value value to be written
     */
    virtual void write_register(const felix::DeviceId& dev_id, const std::string& name, std::uint64_t value) const = 0;

    /**
     * @brief translates fids into device ids. The actual return is in the form of a map so that
     * a relation between fid and device_id can be reconstructed.
     *
     * @throws felix::BusException if any fid was not found
     *
     * @param fids list of fids to be translated
     *
     * @return map of list of fids, accessible by device id
     */
    [[nodiscard]] virtual std::map<felix::DeviceId, std::vector<std::uint64_t>> translate(const std::vector<std::uint64_t>& fids) = 0;
};
