/* -*- mode: c++ -*-
 * Copyright 2025 Dracal Technologies Inc. All rights reserved.
 */

#pragma once

#include <dracal/common/chrono.hpp>
#include <dracal/common/fs.hpp>
#include <dracal/common/log.hpp>
#include <dracal/logging/utils.hpp>

#include <spdlog/logger.h>

#include <atomic>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <tuple>
#include <vector>

class logger_test;

namespace dracal::logging {

/**
 * @brief The interval_logger class for logging data to a file with time-based intervals.
 *
 * The `interval_logger` is a time-based logging system that collects asynchronous data updates and writes them to a
 * file at regular intervals. It uses a dual-bucket architecture (CURRENT and OVERSHOOT) to gracefully handle data that
 * arrives during different time windows, ensuring no data loss even when updates arrive late.
 */
class interval_logger final {
  public:
    using serial = std::string;
    using channel_id = std::string;
    using product_name = std::string;
    using channel_name = std::string;
    using channel_description = std::string;
    using unit_symbol = std::string;
    using value = std::string;
    using line = std::string;

    enum class error_policy {
        write_empty = 0,
        write_previous = 1,
        write_zero = 2,
        write_one = 3,
        write_negative_1 = 4,
        write_error = 5
    };

    /**
     * @brief Constructor for the logger class.
     * @param[in] file The file path where log data will be written.
     * @param[in] interval The time interval between log entries.
     * @param[in] separator The separator string used between values in the log file.
     * @param[in] e_policy The error policy to use when handling missing data.
     * @param[in] f_policy The file policy (overwrite or append).
     * @param[in] comment The comment string to include in the log file header.
     * @param[in] timestamp_column_names The column header names(s) of the timestamp column(s).
     * @param[in] timestamp_convert The function to use to convert the timestamp in the required column(s).
     * @param[in] grace_duration The grace period after the target time before writing.
     * @param[in] sleep_duration The sleep duration for the worker thread.
     * @param[in] memory_max_lines Maximum number of lines to keep in memory (0 = no memory logging).
     * @param[in] infos Vector of channel information tuples (serial, channel_id, product_name, channel_name,
     * channel_description, unit_symbol).
     */
    explicit interval_logger(
        const fs::path &file, const chrono::milliseconds interval, const std::string &separator,
        const error_policy e_policy, const file_policy f_policy, const std::string &comment,
        const std::vector<std::string> &timestamp_column_names,
        const std::function<std::vector<std::string>(const chrono::time_point<chrono::system_clock>)> timestamp_convert,
        const chrono::milliseconds grace_duration, const chrono::milliseconds sleep_duration,
        const size_t memory_max_lines,
        const std::vector<std::tuple<serial, channel_id, product_name, channel_name, channel_description, unit_symbol>>
            &infos);

    /**
     * @brief Destructor for the logger class.
     *
     * Automatically stops the logger and cleans up resources.
     */
    ~interval_logger();

    /**
     * @brief Starts the logger.
     *
     * Initializes the worker thread and begins logging operations.
     * Can only be called once per logger instance.
     *
     * @param[in] now To set the timestamp in the comment section. Defaults to now.
     * @return True if the logger was started successfully, false if already started.
     */
    bool start(const chrono::time_point<chrono::system_clock> now = chrono::system_clock::now());

    /**
     * @brief Stops the logger.
     *
     * Signals the worker thread to stop and waits for it to finish.
     * Safe to call multiple times.
     */
    void stop();

    /**
     * @brief Updates the logger with a new value for a specific channel.
     *
     * Updates the current data for the specified channel. The value will be
     * written to the log file at the next scheduled interval. If the timestamp
     * overshoots the current interval, it will be handled gracefully.
     *
     * @param[in] id The channel identifier.
     * @param[in] timestamp The timestamp of the value.
     * @param[in] v The value to log.
     */
    void update(const channel_id &id, const chrono::time_point<chrono::system_clock> timestamp, const value &v);

    uint64_t number_lines_written() const { return _number_lines_written.load(); }

  private:
    /**
     * @brief Writes the comment section to the log file.
     *
     * Writes the header information to the log file if it hasn't been written yet.
     *
     * @param[in] now The timestamp to use.
     */
    void write_comment_section(const chrono::time_point<chrono::system_clock> now);

    /**
     * @brief Writes the header section to the log file.
     *
     * Writes the header information to the log file if it hasn't been written yet.
     */
    void write_header_section();

    /**
     * @brief Writes a line to the file if needed.
     *
     * Checks if it's time to write a log line based on the current time and
     * grace period. Handles overshoot detection and data staging.
     */
    void write_line_if_needed();

    /**
     * @brief Writes a line to the log file.
     *
     * Formats and writes the current data values to the log file with timestamp.
     */
    void write_line();

    /**
     * @brief Resets the current data.
     *
     * Resets all current data values to their default "no_data" values and
     * advances the target timestamps to the next interval.
     */
    void reset_current();

    /**
     * @brief Applies the overshoot data as the current data.
     *
     * Applies the overshoot data as current and set the flag to trigger a immediate new write.
     */
    void apply_overshoot();

    /**
     * @brief Resets the current data.
     *
     * Resets all current data values to their default "no_data" values or the last known data values.
     */
    void reset_current_data();

  private:
    const std::string _uuid;
    const fs::path _file;
    const chrono::milliseconds _interval;
    const std::string _separator;
    const error_policy _error_policy;
    const file_policy _file_policy;
    const std::string _comment;
    const chrono::milliseconds _grace_duration;
    const chrono::milliseconds _sleep_duration;
    const size_t _memory_max_lines;

    const std::vector<channel_id> _channel_ids;
    const std::vector<product_name> _product_names;
    const std::vector<channel_name> _channel_names;
    const std::vector<channel_description> _channel_descriptions;
    const std::vector<unit_symbol> _unit_symbols;

    const std::vector<std::string> _timestamp_column_names;
    const std::function<std::vector<std::string>(const chrono::time_point<chrono::system_clock>)> _timestamp_convert;

    const std::vector<std::string> _header_section;
    const std::set<channel_id> _ids;

    const std::map<channel_id, bool> _reset_has_data;

    const std::string _last_data_default{""};
    std::map<channel_id, value> _last_data;

    bool _is_started{false};
    bool _comment_section_written{false};
    bool _header_section_written{false};
    bool _first_update_received{false};
    bool _stop_requested{false};
    bool _working_running{false};

    std::atomic<uint64_t> _number_lines_written{0};

    std::vector<std::string> _comment_section;

    std::map<channel_id, value> _current_data;

    chrono::time_point<chrono::system_clock> _last_timestamp_written{};

    // System clock to match the wall clock
    chrono::time_point<chrono::system_clock> _current_timestamp{};
    chrono::time_point<chrono::system_clock> _next_timestamp{};

    // Steady clock to avoid drift
    chrono::time_point<chrono::steady_clock> _current_target{};
    chrono::time_point<chrono::steady_clock> _next_target{};
    chrono::time_point<chrono::steady_clock> _current_grace_period_end{};

    unsigned int _overshoot_number_interval{};

    std::deque<std::map<channel_id, bool>> _overshoot_has_data;
    std::deque<std::map<channel_id, value>> _overshoot_data;

    // System clock to match the wall clock
    std::deque<chrono::time_point<chrono::system_clock>> _overshoot_current_timestamp{};
    std::deque<chrono::time_point<chrono::system_clock>> _overshoot_next_timestamp{};

    // Steady clock to avoid drift
    std::deque<chrono::time_point<chrono::steady_clock>> _overshoot_target{};
    std::deque<chrono::time_point<chrono::steady_clock>> _overshoot_next_target{};
    std::deque<chrono::time_point<chrono::steady_clock>> _overshoot_grace_period_end{};

    std::thread _worker;

    std::shared_ptr<spdlog::logger> _logger;
    std::shared_ptr<memory_sink_mt> _memory_sink;

    mutable std::mutex _mutex;

  public:
    friend class ::logger_test;
};

} // namespace dracal::logging