/**
 * Copyright 2012-2015,2017,2020-2024 Centreon
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * For more information : contact@centreon.com
 */

#include "com/centreon/broker/unified_sql/rebuilder.hh"

#include <fmt/format.h>
#include <cfloat>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <ctime>

#include "com/centreon/broker/misc/time.hh"
#include "com/centreon/broker/multiplexing/publisher.hh"
#include "com/centreon/broker/sql/mysql_error.hh"
#include "com/centreon/broker/sql/mysql_result.hh"
#include "com/centreon/broker/unified_sql/internal.hh"
#include "com/centreon/broker/unified_sql/stream.hh"
#include "com/centreon/exceptions/msg_fmt.hh"
#include "common/log_v2/log_v2.hh"

using namespace com::centreon::exceptions;
using namespace com::centreon::broker;
using namespace com::centreon::broker::unified_sql;

using com::centreon::common::log_v2::log_v2;

/**
 *  Constructor.
 *
 *  @param[in] db_cfg                  Database configuration.
 *  @param[in] rrd_length              Length of RRD files.
 *  @param[in] interval_length         Length in seconds of a time unit.
 */
rebuilder::rebuilder(const database_config& db_cfg,
                     uint32_t rrd_length,
                     uint32_t interval_length)
    : _db_cfg(db_cfg), _interval_length(interval_length), _rrd_len(rrd_length) {
  _db_cfg.set_connections_count(1);
  _db_cfg.set_queries_per_transaction(1);
}

rebuilder::~rebuilder() noexcept {
  log_v2::instance().get(log_v2::SQL)->debug("SQL: stopping rebuilder");
  std::unique_lock<std::mutex> lck(_rebuilding_m);
  _rebuilding_cv.wait_for(lck, std::chrono::seconds(20),
                          [this] { return _rebuilding == 0; });
}

/**
 * @brief process a rebuild metrics message.
 *
 * @param d The BBDO message with all the metric ids to rebuild.
 */
void rebuilder::rebuild_graphs(const std::shared_ptr<io::data>& d,
                               const std::shared_ptr<spdlog::logger>& logger) {
  asio::post(com::centreon::common::pool::io_context(), [this, data = d,
                                                         logger] {
    {
      std::lock_guard<std::mutex> lck(_rebuilding_m);
      _rebuilding++;
      _rebuilding_cv.notify_all();
    }
    const bbdo::pb_rebuild_graphs& ids =
        *static_cast<const bbdo::pb_rebuild_graphs*>(data.get());

    std::string ids_str{
        fmt::format("{}", fmt::join(ids.obj().index_ids(), ","))};
    logger->debug(
        "Metric rebuild: Rebuild metrics event received for metrics ({})",
        ids_str);

    try {
      mysql ms(_db_cfg);
      ms.run_query(
          fmt::format(
              "UPDATE index_data SET must_be_rebuild='2' WHERE id IN ({})",
              ids_str),
          database::mysql_error::update_index_state, false);

      int32_t conn = ms.choose_best_connection(-1);
      /* Lets' get the metrics to rebuild time in DB */
      std::promise<database::mysql_result> promise;
      std::future<database::mysql_result> future = promise.get_future();
      std::string query{fmt::format(
          "SELECT m.metric_id, m.metric_name, m.data_source_type, "
          "i.rrd_retention, s.check_interval, m.index_id FROM metrics m LEFT "
          "JOIN index_data "
          "i ON m.index_id=i.id LEFT JOIN services s ON "
          "i.host_id=s.host_id AND "
          "i.service_id=s.service_id WHERE i.id IN ({})",
          ids_str)};
      logger->trace("Metric rebuild: Executed query << {} >>", query);
      ms.run_query_and_get_result(query, std::move(promise), conn);
      std::map<uint64_t, metric_info> ret_inter;
      std::list<int64_t> mids;
      auto start_rebuild = std::make_shared<storage::pb_rebuild_message>();
      start_rebuild->mut_obj().set_state(RebuildMessage_State_START);
      try {
        database::mysql_result res{future.get()};
        while (ms.fetch_row(res)) {
          uint64_t mid = res.value_as_u64(0);
          uint64_t index_id = res.value_as_u64(5);
          mids.push_back(mid);
          logger->trace("Metric rebuild: metric {} is sent to rebuild", mid);
          (*start_rebuild->mut_obj().mutable_metric_to_index_id())[mid] =
              index_id;
          auto ret = ret_inter.emplace(mid, metric_info());
          metric_info& v = ret.first->second;
          v.metric_name = res.value_as_str(1);
          v.data_source_type = res.value_as_i32(2);
          v.rrd_retention = res.value_as_i32(3);
          if (!v.rrd_retention)
            v.rrd_retention = _rrd_len;
          v.check_interval = res.value_as_f64(4) * _interval_length;
          if (!v.check_interval)
            v.check_interval = 5 * 60;
        }

        if (mids.empty()) {
          logger->error("Metrics rebuild: metrics don't exist: {}", ids_str);
          return;
        }

        std::string mids_str{fmt::format("{}", fmt::join(mids, ","))};
        multiplexing::publisher().write(start_rebuild);

        std::promise<database::mysql_result> promise_c;
        std::future<database::mysql_result> future_c = promise_c.get_future();
        ms.run_query_and_get_result(
            "SELECT len_storage_mysql,len_storage_rrd FROM config",
            std::move(promise_c), conn);
        int32_t db_retention_day = 0;

        res = future_c.get();
        if (ms.fetch_row(res)) {
          db_retention_day = res.value_as_i32(0);
          int32_t db_retention_day1 = res.value_as_i32(1);
          if (db_retention_day1 < db_retention_day)
            db_retention_day = db_retention_day1;
          logger->debug("Storage retention on RRD: {} days", db_retention_day);
        }
        if (db_retention_day) {
          /* Let's get the start of this day */
          struct tm tmv;
          std::time_t now{std::time(nullptr)};
          std::time_t start, end;
          if (localtime_r(&now, &tmv)) {
            /* Let's compute the beginning and the end of the first day where
             * the rebuild starts. */
            tmv.tm_sec = tmv.tm_min = tmv.tm_hour = 0;
            tmv.tm_mday -= db_retention_day;
            start = mktime(&tmv);
            while (db_retention_day >= 0) {
              logger->trace("Metrics rebuild: db_retention_day = {}",
                            db_retention_day);
              tmv.tm_mday++;
              end = mktime(&tmv);
              db_retention_day--;
              std::promise<database::mysql_result> promise_bin;
              std::future<database::mysql_result> future_bin =
                  promise_bin.get_future();
              std::string query{fmt::format(
                  "SELECT id_metric,ctime,value,status FROM data_bin WHERE "
                  "ctime>={} AND "
                  "ctime<{} AND id_metric IN ({}) ORDER BY ctime ASC",
                  start, end, mids_str)};
              logger->trace("Metrics rebuild: Query << {} >> executed", query);
              ms.run_query_and_get_result(query, std::move(promise_bin), conn);
              auto data_rebuild =
                  std::make_shared<storage::pb_rebuild_message>();
              data_rebuild->mut_obj().set_state(RebuildMessage_State_DATA);
              database::mysql_result res(future_bin.get());
              absl::flat_hash_map<uint64_t, time_t> last_inserted;
              while (ms.fetch_row(res)) {
                uint64_t id_metric = res.value_as_u64(0);
                time_t ctime = res.value_as_u64(1);
                float value = res.value_as_f32(2);
                uint32_t status = res.value_as_u32(3);
                // duplicate values not allowed by rrd library
                auto yet_inserted = last_inserted.find(id_metric);
                if (yet_inserted != last_inserted.end()) {
                  if (yet_inserted->second >= ctime) {
                    logger->trace("Metric {} too old to be inserted: {} >= {}",
                                  id_metric, yet_inserted->second, ctime);
                    continue;
                  } else {
                    logger->trace("Metric {} updated at {}", id_metric, ctime);
                    yet_inserted->second = ctime;
                  }
                } else {
                  logger->trace("Metric {} inserted at {}", id_metric, ctime);
                  last_inserted[id_metric] = ctime;
                }
                Point* pt =
                    (*data_rebuild->mut_obj().mutable_timeserie())[id_metric]
                        .add_pts();
                pt->set_ctime(ctime);
                pt->set_value(value);
                pt->set_status(status);
              }
              for (auto it = ret_inter.begin(); it != ret_inter.end(); ++it) {
                auto i = it->second;
                auto& m{
                    (*data_rebuild->mut_obj().mutable_timeserie())[it->first]};
                m.set_check_interval(i.check_interval);
                m.set_data_source_type(i.data_source_type);
                m.set_rrd_retention(i.rrd_retention);
              }
              multiplexing::publisher().write(data_rebuild);
              start = end;
            }
          } else
            throw msg_fmt("Metrics rebuild: Cannot get the date structure.");
        }
      } catch (const std::exception& e) {
        logger->error("Metrics rebuild: Error during metrics rebuild: {}",
                      e.what());
      }
      auto end_rebuild = std::make_shared<storage::pb_rebuild_message>();
      end_rebuild->set_obj(std::move(start_rebuild->mut_obj()));
      end_rebuild->mut_obj().set_state(RebuildMessage_State_END);
      multiplexing::publisher().write(end_rebuild);
      ms.run_query(
          fmt::format(
              "UPDATE index_data SET must_be_rebuild='0' WHERE id IN ({})",
              ids_str),
          database::mysql_error::update_index_state, false);
      logger->debug(
          "Metric rebuild: Rebuild of metrics from the following indexes ({}) "
          "finished",
          ids_str);
    } catch (const std::exception& e) {
      logger->error("Metric rebuild: error with the database: {}", e.what());
    }
    {
      std::lock_guard<std::mutex> lck(_rebuilding_m);
      _rebuilding--;
      _rebuilding_cv.notify_all();
    }
  });
}
