#include "storage/io_module_store.hpp"

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

#include <sqlite3.h>

namespace lm {

namespace {

nlohmann::json rowToJson(sqlite3_stmt* stmt)
{
  auto text = [&](int col) -> std::string {
    if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
      return "";
    const char* v = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
    return v ? std::string(v) : "";
  };
  return {{"id", text(0)},
          {"site_id", text(1)},
          {"name", text(2)},
          {"module_type", text(3)},
          {"ip_address", text(4)},
          {"created_by", text(5)},
          {"created_by_group", text(6)},
          {"connected", sqlite3_column_int(stmt, 7) != 0},
          {"created_at", text(8)},
          {"updated_at", text(9)}};
}

constexpr const char* kSelect =
    "SELECT id, site_id, name, module_type, ip_address, created_by, created_by_group, connected, created_at, updated_at "
    "FROM io_modules";

bool validModuleType(const std::string& t)
{
  return t == "bluetooth" || t == "wise";
}

}  // namespace

IoModuleStore::IoModuleStore(Database& db) : db_(db) {}

bool IoModuleStore::findNameConflictUnlocked(const std::string& site_id,
                                             const std::string& name,
                                             const std::string& except_id) const
{
  const std::string needle = StringUtil::toLower(StringUtil::trimCopy(name));
  if (needle.empty())
    return false;

  sqlite3_stmt* stmt = nullptr;
  const char* sql = except_id.empty()
                        ? "SELECT id FROM io_modules WHERE site_id = ?1 AND lower(name) = lower(?2) LIMIT 1"
                        : "SELECT id FROM io_modules WHERE site_id = ?1 AND lower(name) = lower(?2) AND id != ?3 LIMIT 1";
  if (sqlite3_prepare_v2(db_.handle(), sql, -1, &stmt, nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, site_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
  if (!except_id.empty())
    sqlite3_bind_text(stmt, 3, except_id.c_str(), -1, SQLITE_TRANSIENT);
  const bool conflict = sqlite3_step(stmt) == SQLITE_ROW;
  sqlite3_finalize(stmt);
  return conflict;
}

nlohmann::json IoModuleStore::list(const std::string& site_id) const
{
  std::lock_guard<std::mutex> lock(mu_);
  nlohmann::json out = nlohmann::json::array();
  std::string sql = kSelect;
  if (!site_id.empty())
    sql += " WHERE site_id = ?1";
  sql += " ORDER BY name";

  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
    return out;
  if (!site_id.empty())
    sqlite3_bind_text(stmt, 1, site_id.c_str(), -1, SQLITE_TRANSIENT);
  while (sqlite3_step(stmt) == SQLITE_ROW)
    out.push_back(rowToJson(stmt));
  sqlite3_finalize(stmt);
  return out;
}

std::optional<nlohmann::json> IoModuleStore::find(const std::string& id) const
{
  std::lock_guard<std::mutex> lock(mu_);
  const std::string sql = std::string(kSelect) + " WHERE id = ?1";
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
    return std::nullopt;
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  std::optional<nlohmann::json> out;
  if (sqlite3_step(stmt) == SQLITE_ROW)
    out = rowToJson(stmt);
  sqlite3_finalize(stmt);
  return out;
}

std::optional<nlohmann::json> IoModuleStore::findByName(const std::string& name) const
{
  const std::string needle = StringUtil::toLower(StringUtil::trimCopy(name));
  if (needle.empty())
    return std::nullopt;
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "SELECT id, site_id, name, module_type, ip_address, created_by, created_by_group, connected, "
                         "created_at, updated_at FROM io_modules WHERE lower(name) = lower(?1) LIMIT 1",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return std::nullopt;
  sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
  std::optional<nlohmann::json> out;
  if (sqlite3_step(stmt) == SQLITE_ROW)
    out = rowToJson(stmt);
  sqlite3_finalize(stmt);
  return out;
}

std::optional<nlohmann::json> IoModuleStore::resolveRef(const std::string& ref) const
{
  if (auto by_id = find(ref))
    return by_id;
  return findByName(ref);
}

std::optional<nlohmann::json> IoModuleStore::create(const nlohmann::json& payload, std::string& err)
{
  if (!payload.is_object())
  {
    err = "payload must be an object";
    return std::nullopt;
  }

  const std::string site_id = StringUtil::trimCopy(payload.value("site_id", ""));
  const std::string name = StringUtil::trimCopy(payload.value("name", ""));
  std::string module_type = StringUtil::toLower(StringUtil::trimCopy(payload.value("module_type", "")));
  const std::string ip_address = StringUtil::trimCopy(payload.value("ip_address", ""));
  const std::string created_by = StringUtil::trimCopy(payload.value("created_by", ""));
  const std::string created_by_group = StringUtil::trimCopy(payload.value("created_by_group", ""));

  if (site_id.empty() || name.empty() || module_type.empty() || ip_address.empty())
  {
    err = "missing required fields";
    return std::nullopt;
  }
  if (!validModuleType(module_type))
  {
    err = "module_type must be bluetooth or wise";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (findNameConflictUnlocked(site_id, name, ""))
  {
    err = "io module name already exists for this site";
    return std::nullopt;
  }

  const std::string id = payload.value("id", IdUtil::newId());
  const std::string now = IdUtil::nowIso8601();

  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT INTO io_modules(id, site_id, name, module_type, ip_address, created_by, "
                         "created_by_group, connected, created_at, updated_at) "
                         "VALUES(?1,?2,?3,?4,?5,?6,?7,0,?8,?9)",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
  {
    err = sqlite3_errmsg(db_.handle());
    return std::nullopt;
  }
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, site_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, name.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 4, module_type.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 5, ip_address.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 6, created_by.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 7, created_by_group.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 8, now.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 9, now.c_str(), -1, SQLITE_TRANSIENT);
  if (sqlite3_step(stmt) != SQLITE_DONE)
  {
    err = sqlite3_errmsg(db_.handle());
    sqlite3_finalize(stmt);
    return std::nullopt;
  }
  sqlite3_finalize(stmt);

  return nlohmann::json{{"id", id},
                        {"site_id", site_id},
                        {"name", name},
                        {"module_type", module_type},
                        {"ip_address", ip_address},
                        {"created_by", created_by},
                        {"created_by_group", created_by_group},
                        {"connected", false},
                        {"created_at", now},
                        {"updated_at", now}};
}

bool IoModuleStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
{
  auto existing = find(id);
  if (!existing)
  {
    err = "io module not found";
    return false;
  }

  nlohmann::json merged = *existing;
  for (const char* key : {"site_id", "name", "module_type", "ip_address"})
  {
    if (payload.contains(key))
      merged[key] = payload[key];
  }

  const std::string site_id = StringUtil::trimCopy(merged.value("site_id", ""));
  const std::string name = StringUtil::trimCopy(merged.value("name", ""));
  std::string module_type = StringUtil::toLower(StringUtil::trimCopy(merged.value("module_type", "")));
  const std::string ip_address = StringUtil::trimCopy(merged.value("ip_address", ""));

  if (site_id.empty() || name.empty() || module_type.empty() || ip_address.empty())
  {
    err = "missing required fields";
    return false;
  }
  if (!validModuleType(module_type))
  {
    err = "module_type must be bluetooth or wise";
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (findNameConflictUnlocked(site_id, name, id))
  {
    err = "io module name already exists for this site";
    return false;
  }

  const std::string now = IdUtil::nowIso8601();
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "UPDATE io_modules SET site_id=?2, name=?3, module_type=?4, ip_address=?5, updated_at=?6 WHERE id=?1",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
  {
    err = sqlite3_errmsg(db_.handle());
    return false;
  }
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, site_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, name.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 4, module_type.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 5, ip_address.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  if (!ok)
    err = sqlite3_errmsg(db_.handle());
  sqlite3_finalize(stmt);
  return ok;
}

bool IoModuleStore::remove(const std::string& id, std::string& err)
{
  if (!find(id))
  {
    err = "io module not found";
    return false;
  }
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM io_modules WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
  {
    err = sqlite3_errmsg(db_.handle());
    return false;
  }
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  if (!ok)
    err = sqlite3_errmsg(db_.handle());
  sqlite3_finalize(stmt);
  return ok;
}

bool IoModuleStore::setConnected(const std::string& id, bool connected, std::string& err)
{
  if (!find(id))
  {
    err = "io module not found";
    return false;
  }
  const std::string now = IdUtil::nowIso8601();
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "UPDATE io_modules SET connected=?2, updated_at=?3 WHERE id=?1",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
  {
    err = sqlite3_errmsg(db_.handle());
    return false;
  }
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_int(stmt, 2, connected ? 1 : 0);
  sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  if (!ok)
    err = sqlite3_errmsg(db_.handle());
  sqlite3_finalize(stmt);
  return ok;
}

}  // namespace lm
