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

#include <dracal/common/fmt.hpp>
#include <dracal/common/log.hpp>
#include <dracal/common/string.hpp>
#include <dracal/logging/logger/interval.hpp>
#include <dracal/logging/utils.hpp>

#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/spdlog.h>

#include <ranges>

namespace dracal::logging {

interval_logger::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)
    : _uuid(str::uuid_v4()), _file(file), _interval(interval), _separator(separator), _error_policy(e_policy),
      _file_policy(f_policy), _comment(comment), _grace_duration(grace_duration), _sleep_duration(sleep_duration),
      _memory_max_lines(memory_max_lines), _channel_ids([&infos]() -> std::vector<channel_id> {
          std::vector<channel_id> result;
          result.reserve(infos.size());
          for (const auto &info : infos) {
              result.push_back(std::get<1>(info));
          }
          return result;
      }()),
      _product_names([&infos]() -> std::vector<product_name> {
          std::vector<product_name> result;
          result.reserve(infos.size());
          for (const auto &info : infos) {
              result.push_back(std::get<2>(info));
          }
          return result;
      }()),
      _channel_names([&infos]() -> std::vector<channel_name> {
          std::vector<channel_name> result;
          result.reserve(infos.size());
          for (const auto &info : infos) {
              result.push_back(std::get<3>(info));
          }
          return result;
      }()),
      _channel_descriptions([&infos]() -> std::vector<channel_description> {
          std::vector<channel_description> result;
          result.reserve(infos.size());
          for (const auto &info : infos) {
              result.push_back(std::get<4>(info));
          }
          return result;
      }()),
      _unit_symbols([&infos]() -> std::vector<unit_symbol> {
          std::vector<unit_symbol> result;
          result.reserve(infos.size());
          for (const auto &info : infos) {
              result.push_back(std::get<5>(info));
          }
          return result;
      }()),
      _timestamp_column_names(timestamp_column_names), _timestamp_convert(timestamp_convert),
      _header_section([this]() -> std::vector<std::string> {
          return utils::create_header_section(_separator, _timestamp_column_names, _product_names, _channel_names,
                                              _channel_descriptions, _unit_symbols);
      }()),
      _ids([&infos]() -> std::set<channel_id> {
          std::set<channel_id> result;
          for (const auto &info : infos) {
              result.insert(std::get<1>(info));
          }
          return result;
      }()),
      _reset_has_data([this]() -> std::map<channel_id, bool> {
          std::map<channel_id, bool> result;
          for (const auto &id : _channel_ids) {
              result[id] = false;
          }
          return result;
      }()) {

    for (const auto &id : _channel_ids) {
        _last_data[id] = _last_data_default;
    }

    if (_timestamp_convert) {
        const auto now = chrono::system_clock::now();
        const auto timestamps = _timestamp_convert(now);

        if (timestamps.size() != _timestamp_column_names.size()) {
            dracal_warn(
                "mismatch between timestamp convert output({}) and timestamp column names({}) respective sizes.",
                timestamps.size(), _timestamp_column_names);
        }
    }

    reset_current_data();
}

interval_logger::~interval_logger() { stop(); }

bool interval_logger::start(const chrono::time_point<chrono::system_clock> now) {
    if (_is_started) {
        dracal_warn("logger already started: skipping...");
        return false;
    }

    if (_stop_requested) {
        dracal_warn("logger cannot be restarted after being stopped");
        return false;
    }

    if (!_timestamp_convert) {
        dracal_warn("timestamp convert function is nullptr: skipping...");
        return false;
    }

    try {
        auto file_sink =
            std::make_shared<spdlog::sinks::basic_file_sink_mt>(_file.string(), _file_policy == file_policy::overwrite);
        _memory_sink = std::make_shared<memory_sink_mt>(_memory_max_lines);

        _logger = std::make_shared<spdlog::logger>(_uuid, spdlog::sinks_init_list{file_sink, _memory_sink});
        _logger->set_pattern("%v");
        _logger->set_level(spdlog::level::info);
        spdlog::register_logger(_logger);
    } catch (const spdlog::spdlog_ex &ex) {
        dracal_error("failed to create logger with spdlog error: ({})", ex.what());
        return false;
    } catch (const std::exception &ex) {
        dracal_error("failed to create logger with error: ({})", ex.what());
        return false;
    } catch (...) {
        dracal_error("failed to create logger with unknown error");
        return false;
    }

    write_comment_section(now);
    write_header_section();

    _is_started = true;
    _working_running = true;

    _worker = std::thread([this]() {
        while (!_stop_requested) {
            {
                std::lock_guard<std::mutex> guard(_mutex);
                write_line_if_needed();
            }

            std::this_thread::sleep_for(_sleep_duration);
        }

        _working_running = false;
    });

    return true;
}

void interval_logger::stop() {
    if (!_is_started) {
        return;
    }

    _stop_requested = true;
    _worker.join();

    if (_logger) {
        _logger->flush();
        spdlog::drop(_uuid);
        _logger.reset();
    }

    _is_started = false;
    dracal_info("logger stopped");
}

void interval_logger::update(const channel_id &id, const chrono::time_point<chrono::system_clock> timestamp,
                             const value &v) {
    std::lock_guard<std::mutex> guard(_mutex);

    if (!_is_started) {
        dracal_warn("logger not started: skipping...");
        return;
    }

    if (_ids.count(id) == 0) {
        dracal_debug("channel id({}) not found: skipping...", id);
        return;
    }

    if (timestamp <= _last_timestamp_written) {
        dracal_warn("update for channel id({}) is too late{}, last write was on({}): skipping...", id, timestamp,
                    _last_timestamp_written);
        return;
    }

    if (!_first_update_received) {
        dracal_info("first interval starts: {}", timestamp);
        _current_timestamp = timestamp;
        _next_timestamp = _current_timestamp + _interval;

        _current_target = chrono::steady_clock::now();
        _next_target = _current_target + _interval;
        _current_grace_period_end = _current_target + _interval + _grace_duration;

        _first_update_received = true;
    }

    const bool overshoot = timestamp >= _next_timestamp;

    // Overshoot: should never happen, but if so we gracefully handle it.
    if (overshoot) {
        const auto overshoot_number_interval = (timestamp - _current_timestamp) / _interval;
        const auto overshoot_index = overshoot_number_interval - 1;

        dracal_debug("overshoot detected in advance of N({}) interval{}", overshoot_number_interval,
                     overshoot_number_interval > 1 ? "s" : "");

        if (_overshoot_number_interval < overshoot_number_interval) {
            // Fill the gap: initialize every overshoot interval from last overshoot interval on record
            // up to the new overshoot interval (inclusive).
            for (unsigned int i = _overshoot_number_interval; i < overshoot_number_interval; ++i) {
                const auto &index = i;
                const auto &number_interval = i + 1;

                _overshoot_has_data.push_back(_reset_has_data);
                _overshoot_data.push_back({});

                _overshoot_current_timestamp.push_back(_current_timestamp + (number_interval * _interval));
                _overshoot_next_timestamp.push_back(_overshoot_current_timestamp[index] + _interval);
                _overshoot_target.push_back(_current_target + (number_interval * _interval));
                _overshoot_next_target.push_back(_overshoot_target[index] + _interval);
                _overshoot_grace_period_end.push_back(_overshoot_target[index] + _interval + _grace_duration);
            }

            _overshoot_number_interval = _overshoot_data.size();
        }

        dracal_trace("update channel: overshoot({}) id({}) with timestamp({})", overshoot_number_interval, id,
                     timestamp);
        _overshoot_has_data[overshoot_index][id] = true;
        _overshoot_data[overshoot_index][id] = v;
        return;
    }

    // Overshoot is already handled we only need to care for the undershoot.
    if (timestamp < _current_timestamp) {
        dracal_debug("timestamp({}) is before current timestamp({})", timestamp, _current_timestamp);
        return;
    }

    dracal_trace("timestamp {}, current {}, next {}", timestamp, _current_timestamp, _next_timestamp);

    dracal_trace("update channel: id({}) with timestamp({})", id, timestamp);
    _current_data[id] = v;
    _last_data[id] = v;
}

void interval_logger::write_comment_section(const chrono::time_point<chrono::system_clock> now) {
    if (_comment_section_written) {
        dracal_warn("comment section already written: skipping...");
        return;
    }

    _comment_section = utils::create_comment_section(now, _interval, _comment);

    if (!_logger) {
        dracal_warn("logger is nullptr");
        return;
    }

    for (const auto &line : _comment_section) {
        _logger->info(line);
    }

    _comment_section_written = true;
}

void interval_logger::write_header_section() {
    if (_header_section_written) {
        dracal_warn("header section already written: skipping...");
        return;
    }

    if (!_logger) {
        dracal_warn("logger is nullptr");
        return;
    }

    for (const auto &line : _header_section) {
        _logger->info(line);
    }

    _header_section_written = true;
}

void interval_logger::write_line_if_needed() {
    if (!_first_update_received) {
        return;
    }

    const auto now = chrono::steady_clock::now();

    if (now > _current_grace_period_end) {
        dracal_trace("current grace period end reached: writing current line");
        write_line();
        apply_overshoot();
    }
}

void interval_logger::write_line() {
    if (!_logger) {
        dracal_warn("logger not set: skipping...");
        return;
    }

#if __cplusplus >= 202002L
    const auto values = _channel_ids | std::views::transform([this](const auto &id) { return _current_data[id]; });
#else
    std::vector<std::string> values;
    values.reserve(_channel_ids.size());
    for (const auto &id : _channel_ids) {
        values.push_back(_current_data[id]);
    }
#endif

    const auto date_time = _timestamp_convert(_current_timestamp);
    const std::string line = fmt::format("{}{}{}", fmt::join(date_time, _separator),
                                         date_time.size() == 0 ? "" : _separator, fmt::join(values, _separator));

    _logger->info(line);
    _last_timestamp_written = _current_timestamp;
    _number_lines_written.fetch_add(1);

    dracal_trace("wrote line: {}", line);
    reset_current();
}

void interval_logger::reset_current() {
    reset_current_data();

    _current_timestamp = _next_timestamp;
    _next_timestamp = _next_timestamp + _interval;

    _current_target = _next_target;
    _next_target = _current_target + _interval;
    _current_grace_period_end = _current_target + _interval + _grace_duration;

    dracal_trace("reset current");
}

void interval_logger::apply_overshoot() {
    if (_overshoot_number_interval == 0) {
        dracal_trace("overshoot queue is empty: nothing to do");
        return;
    }

    // Ensuring that we update the last data accordingly when applying overshoot.
    for (const auto &id : _channel_ids) {
        if (_overshoot_has_data.front()[id]) {
            _last_data[id] = _overshoot_data.front()[id];
        }
    }

    reset_current_data();

    for (const auto &id : _channel_ids) {
        if (_overshoot_has_data.front()[id]) {
            _current_data[id] = _overshoot_data.front()[id];
        }
    }

    _current_timestamp = _overshoot_current_timestamp.front();
    _next_timestamp = _overshoot_next_timestamp.front();

    _current_target = _overshoot_target.front();
    _next_target = _overshoot_next_target.front();
    _current_grace_period_end = _overshoot_grace_period_end.front();
    dracal_trace("applied overshoot");

    _overshoot_has_data.pop_front();
    _overshoot_data.pop_front();

    _overshoot_current_timestamp.pop_front();
    _overshoot_next_timestamp.pop_front();

    _overshoot_target.pop_front();
    _overshoot_next_target.pop_front();
    _overshoot_grace_period_end.pop_front();

    _overshoot_number_interval = _overshoot_has_data.size();
    dracal_trace("pop front overshoot, remaining overshoot queue size({})", _overshoot_number_interval);
}

void interval_logger::reset_current_data() {
    switch (_error_policy) {
    case error_policy::write_empty:
        for (const auto &id : _channel_ids) {
            _current_data[id] = std::string("");
        }
        return;
    case error_policy::write_previous:
        for (const auto &id : _channel_ids) {
            _current_data[id] = _last_data[id];
        }
        return;
    case error_policy::write_zero:
        for (const auto &id : _channel_ids) {
            _current_data[id] = std::string("0");
        }
        return;
    case error_policy::write_one:
        for (const auto &id : _channel_ids) {
            _current_data[id] = std::string("1");
        }
        return;
    case error_policy::write_negative_1:
        for (const auto &id : _channel_ids) {
            _current_data[id] = std::string("-1");
        }
        return;
    case error_policy::write_error:
        for (const auto &id : _channel_ids) {
            _current_data[id] = std::string("error");
        }
        return;
    }
}

} // namespace dracal::logging