#include "storage/dashboard_store.hpp"

#include "storage/database.hpp"
#include "util/id_util.hpp"

#include <sqlite3.h>

namespace lm {

namespace {

const char* kDefaultId = "dashboard_default";

nlohmann::json loadWidgets(sqlite3* db, const std::string& dashboard_id)
{
  nlohmann::json widgets = nlohmann::json::array();
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db,
                         "SELECT id, type, title, mission_id, mission_group, config_json, sort_order "
                         "FROM dashboard_widgets WHERE dashboard_id = ?1 ORDER BY sort_order, id",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return widgets;
  sqlite3_bind_text(stmt, 1, dashboard_id.c_str(), -1, SQLITE_TRANSIENT);
  while (sqlite3_step(stmt) == SQLITE_ROW)
  {
    nlohmann::json w;
    w["id"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
    w["type"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
    w["title"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
    if (sqlite3_column_type(stmt, 3) != SQLITE_NULL)
      w["mission_id"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
    if (sqlite3_column_type(stmt, 4) != SQLITE_NULL)
      w["group"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
    const char* cfg = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5));
    if (cfg)
    {
      try
      {
        const auto extra = nlohmann::json::parse(cfg);
        if (extra.is_object())
        {
          for (auto it = extra.begin(); it != extra.end(); ++it)
          {
            if (!w.contains(it.key()))
              w[it.key()] = it.value();
          }
        }
      }
      catch (...)
      {
      }
    }
    widgets.push_back(w);
  }
  sqlite3_finalize(stmt);
  return widgets;
}

nlohmann::json loadEditGroups(sqlite3* db, const std::string& dashboard_id)
{
  nlohmann::json groups = nlohmann::json::array();
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db,
                         "SELECT group_id FROM dashboard_edit_groups WHERE dashboard_id = ?1 ORDER BY group_id",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return groups;
  sqlite3_bind_text(stmt, 1, dashboard_id.c_str(), -1, SQLITE_TRANSIENT);
  while (sqlite3_step(stmt) == SQLITE_ROW)
    groups.push_back(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)));
  sqlite3_finalize(stmt);
  return groups;
}

std::optional<std::string> loadActiveDashboardId(sqlite3* db)
{
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db, "SELECT active_dashboard_id FROM dashboard_state WHERE id = 1", -1, &stmt, nullptr) !=
      SQLITE_OK)
    return std::nullopt;
  std::optional<std::string> out;
  if (sqlite3_step(stmt) == SQLITE_ROW && sqlite3_column_type(stmt, 0) != SQLITE_NULL)
    out = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
  sqlite3_finalize(stmt);
  return out;
}

}  // namespace

DashboardStore::DashboardStore(Database& db) : db_(db)
{
  std::lock_guard<std::mutex> lock(mu_);
  ensureDefaultsUnlocked();
}

void DashboardStore::ensureDefaultsUnlocked()
{
  sqlite3_stmt* count_stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), "SELECT COUNT(*) FROM dashboards", -1, &count_stmt, nullptr) != SQLITE_OK)
    return;
  int count = 0;
  if (sqlite3_step(count_stmt) == SQLITE_ROW)
    count = sqlite3_column_int(count_stmt, 0);
  sqlite3_finalize(count_stmt);
  if (count > 0)
    return;

  const std::string now = IdUtil::nowIso8601();
  sqlite3_stmt* ins = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT INTO dashboards(id, name, created_by, created_by_user, is_default, sort_order, "
                         "created_at, updated_at) VALUES(?1,?2,?3,NULL,1,0,?4,?4)",
                         -1,
                         &ins,
                         nullptr) != SQLITE_OK)
    return;
  sqlite3_bind_text(ins, 1, kDefaultId, -1, SQLITE_STATIC);
  sqlite3_bind_text(ins, 2, "Default Dashboard", -1, SQLITE_STATIC);
  sqlite3_bind_text(ins, 3, "MiR", -1, SQLITE_STATIC);
  sqlite3_bind_text(ins, 4, now.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_step(ins);
  sqlite3_finalize(ins);

  for (const char* gid :
       {"group_administrators", "group_distributors", "group_users"})
  {
    sqlite3_stmt* g = nullptr;
    if (sqlite3_prepare_v2(db_.handle(),
                           "INSERT OR IGNORE INTO dashboard_edit_groups(dashboard_id, group_id) VALUES(?1,?2)",
                           -1,
                           &g,
                           nullptr) != SQLITE_OK)
      continue;
    sqlite3_bind_text(g, 1, kDefaultId, -1, SQLITE_STATIC);
    sqlite3_bind_text(g, 2, gid, -1, SQLITE_STATIC);
    sqlite3_step(g);
    sqlite3_finalize(g);
  }

  sqlite3_stmt* st = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT OR REPLACE INTO dashboard_state(id, active_dashboard_id) VALUES(1, ?1)",
                         -1,
                         &st,
                         nullptr) == SQLITE_OK)
  {
    sqlite3_bind_text(st, 1, kDefaultId, -1, SQLITE_STATIC);
    sqlite3_step(st);
    sqlite3_finalize(st);
  }
}

nlohmann::json DashboardStore::snapshot() const
{
  std::lock_guard<std::mutex> lock(mu_);
  nlohmann::json dashboards = nlohmann::json::array();
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "SELECT id, name, created_by, created_by_user, is_default FROM dashboards ORDER BY sort_order, name",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return {{"dashboards", dashboards}, {"activeDashboardId", nullptr}};

  while (sqlite3_step(stmt) == SQLITE_ROW)
  {
    const std::string id = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
    nlohmann::json d;
    d["id"] = id;
    d["name"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
    d["createdBy"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
    if (sqlite3_column_type(stmt, 3) != SQLITE_NULL)
      d["createdByUser"] = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
    else
      d["createdByUser"] = nullptr;
    d["isDefault"] = sqlite3_column_int(stmt, 4) != 0;
    d["editGroups"] = loadEditGroups(db_.handle(), id);
    d["widgets"] = loadWidgets(db_.handle(), id);
    dashboards.push_back(d);
  }
  sqlite3_finalize(stmt);

  std::string active_id = kDefaultId;
  if (auto active = loadActiveDashboardId(db_.handle()))
    active_id = *active;
  if (dashboards.empty())
    return {{"dashboards", nlohmann::json::array()}, {"activeDashboardId", nullptr}};
  return {{"dashboards", dashboards}, {"activeDashboardId", active_id}};
}

bool DashboardStore::replace(const nlohmann::json& payload, std::string& err)
{
  if (!payload.is_object())
  {
    err = "payload must be an object";
    return false;
  }
  if (!payload.contains("dashboards") || !payload["dashboards"].is_array())
  {
    err = "dashboards array is required";
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  sqlite3* db = db_.handle();
  char* msg = nullptr;
  if (sqlite3_exec(db, "BEGIN IMMEDIATE", nullptr, nullptr, &msg) != SQLITE_OK)
  {
    err = msg ? msg : "begin failed";
    sqlite3_free(msg);
    return false;
  }

  auto rollback = [&]() {
    sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr);
  };

  if (sqlite3_exec(db, "DELETE FROM dashboard_widgets", nullptr, nullptr, &msg) != SQLITE_OK ||
      sqlite3_exec(db, "DELETE FROM dashboard_edit_groups", nullptr, nullptr, &msg) != SQLITE_OK ||
      sqlite3_exec(db, "DELETE FROM dashboards", nullptr, nullptr, &msg) != SQLITE_OK)
  {
    err = msg ? msg : "clear failed";
    sqlite3_free(msg);
    rollback();
    return false;
  }

  const std::string now = IdUtil::nowIso8601();
  int sort = 0;
  for (const auto& d : payload["dashboards"])
  {
    if (!d.is_object() || !d.contains("id"))
      continue;
    const std::string id = d.value("id", IdUtil::newId());
    sqlite3_stmt* ins = nullptr;
    if (sqlite3_prepare_v2(db,
                           "INSERT INTO dashboards(id, name, created_by, created_by_user, is_default, sort_order, "
                           "created_at, updated_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?7)",
                           -1,
                           &ins,
                           nullptr) != SQLITE_OK)
    {
      rollback();
      err = "insert dashboard failed";
      return false;
    }
    sqlite3_bind_text(ins, 1, id.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ins, 2, d.value("name", "Dashboard").c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ins, 3, d.value("createdBy", "").c_str(), -1, SQLITE_TRANSIENT);
    if (d.contains("createdByUser") && !d["createdByUser"].is_null())
      sqlite3_bind_text(ins, 4, d["createdByUser"].get<std::string>().c_str(), -1, SQLITE_TRANSIENT);
    else
      sqlite3_bind_null(ins, 4);
    sqlite3_bind_int(ins, 5, d.value("isDefault", false) ? 1 : 0);
    sqlite3_bind_int(ins, 6, sort++);
    sqlite3_bind_text(ins, 7, now.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_step(ins);
    sqlite3_finalize(ins);

    if (d.contains("editGroups") && d["editGroups"].is_array())
    {
      for (const auto& g : d["editGroups"])
      {
        if (!g.is_string())
          continue;
        sqlite3_stmt* gs = nullptr;
        if (sqlite3_prepare_v2(db,
                               "INSERT INTO dashboard_edit_groups(dashboard_id, group_id) VALUES(?1,?2)",
                               -1,
                               &gs,
                               nullptr) == SQLITE_OK)
        {
          sqlite3_bind_text(gs, 1, id.c_str(), -1, SQLITE_TRANSIENT);
          sqlite3_bind_text(gs, 2, g.get<std::string>().c_str(), -1, SQLITE_TRANSIENT);
          sqlite3_step(gs);
          sqlite3_finalize(gs);
        }
      }
    }

    if (d.contains("widgets") && d["widgets"].is_array())
    {
      int wsort = 0;
      for (const auto& w : d["widgets"])
      {
        if (!w.is_object())
          continue;
        nlohmann::json config = nlohmann::json::object();
        for (auto it = w.begin(); it != w.end(); ++it)
        {
          if (it.key() != "id" && it.key() != "type" && it.key() != "title" && it.key() != "mission_id" &&
              it.key() != "group")
            config[it.key()] = it.value();
        }
        sqlite3_stmt* ws = nullptr;
        if (sqlite3_prepare_v2(db,
                               "INSERT INTO dashboard_widgets(id, dashboard_id, type, title, mission_id, mission_group, "
                               "config_json, sort_order) VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
                               -1,
                               &ws,
                               nullptr) != SQLITE_OK)
          continue;
        const std::string wid = w.value("id", IdUtil::newId());
        sqlite3_bind_text(ws, 1, wid.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(ws, 2, id.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(ws, 3, w.value("type", "unknown").c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(ws, 4, w.value("title", "").c_str(), -1, SQLITE_TRANSIENT);
        if (w.contains("mission_id") && w["mission_id"].is_string())
          sqlite3_bind_text(ws, 5, w["mission_id"].get<std::string>().c_str(), -1, SQLITE_TRANSIENT);
        else
          sqlite3_bind_null(ws, 5);
        if (w.contains("group") && w["group"].is_string())
          sqlite3_bind_text(ws, 6, w["group"].get<std::string>().c_str(), -1, SQLITE_TRANSIENT);
        else
          sqlite3_bind_null(ws, 6);
        const std::string cfg = config.dump();
        sqlite3_bind_text(ws, 7, cfg.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_int(ws, 8, wsort++);
        sqlite3_step(ws);
        sqlite3_finalize(ws);
      }
    }
  }

  const std::string active = payload.value("activeDashboardId", kDefaultId);
  sqlite3_stmt* ast = nullptr;
  if (sqlite3_prepare_v2(db,
                         "INSERT OR REPLACE INTO dashboard_state(id, active_dashboard_id) VALUES(1, ?1)",
                         -1,
                         &ast,
                         nullptr) == SQLITE_OK)
  {
    sqlite3_bind_text(ast, 1, active.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_step(ast);
    sqlite3_finalize(ast);
  }

  if (sqlite3_exec(db, "COMMIT", nullptr, nullptr, &msg) != SQLITE_OK)
  {
    err = msg ? msg : "commit failed";
    sqlite3_free(msg);
    rollback();
    return false;
  }
  return true;
}

}  // namespace lm
