#include "storage/path_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) : "";
  };
  nlohmann::json points = nlohmann::json::array();
  try
  {
    const std::string raw = text(6);
    if (!raw.empty())
      points = nlohmann::json::parse(raw);
  }
  catch (...)
  {
    points = nlohmann::json::array();
  }
  return {{"id", text(0)},
          {"site_id", text(1)},
          {"map_id", text(2)},
          {"from_position_id", text(3)},
          {"to_position_id", text(4)},
          {"auto_created", sqlite3_column_int(stmt, 5) != 0},
          {"points", points.is_array() ? points : nlohmann::json::array()},
          {"created_at", text(7)},
          {"updated_at", text(8)}};
}

constexpr const char* kSelect =
    "SELECT id, site_id, map_id, from_position_id, to_position_id, auto_created, points_json, created_at, updated_at "
    "FROM paths";

}  // namespace

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

nlohmann::json PathStore::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 map_id, from_position_id, to_position_id";

  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> PathStore::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> PathStore::findBetween(const std::string& map_id,
                                                     const std::string& from_position_id,
                                                     const std::string& to_position_id) const
{
  if (map_id.empty() || from_position_id.empty() || to_position_id.empty())
    return std::nullopt;
  std::lock_guard<std::mutex> lock(mu_);
  const std::string sql = std::string(kSelect) +
                          " WHERE map_id = ?1 AND from_position_id = ?2 AND to_position_id = ?3 LIMIT 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, map_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, from_position_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, to_position_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> PathStore::upsert(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 map_id = StringUtil::trimCopy(payload.value("map_id", ""));
  const std::string from_id = StringUtil::trimCopy(payload.value("from_position_id", ""));
  const std::string to_id = StringUtil::trimCopy(payload.value("to_position_id", ""));
  if (site_id.empty() || map_id.empty() || from_id.empty() || to_id.empty())
  {
    err = "missing required fields";
    return std::nullopt;
  }
  if (from_id == to_id)
  {
    err = "from and to positions must differ";
    return std::nullopt;
  }
  const auto& points = payload.contains("points") && payload["points"].is_array() ? payload["points"]
                                                                                  : nlohmann::json::array();
  const std::string points_json = points.dump();
  const bool auto_created = payload.value("auto_created", true);
  const std::string now = IdUtil::nowIso8601();

  std::string id = payload.value("id", IdUtil::newId());
  {
    std::lock_guard<std::mutex> lock(mu_);
    sqlite3_stmt* lookup = nullptr;
    if (sqlite3_prepare_v2(db_.handle(),
                           "SELECT id FROM paths WHERE map_id = ?1 AND from_position_id = ?2 AND to_position_id = ?3",
                           -1,
                           &lookup,
                           nullptr) == SQLITE_OK)
    {
      sqlite3_bind_text(lookup, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
      sqlite3_bind_text(lookup, 2, from_id.c_str(), -1, SQLITE_TRANSIENT);
      sqlite3_bind_text(lookup, 3, to_id.c_str(), -1, SQLITE_TRANSIENT);
      if (sqlite3_step(lookup) == SQLITE_ROW)
      {
        const char* existing = reinterpret_cast<const char*>(sqlite3_column_text(lookup, 0));
        if (existing)
          id = existing;
      }
      sqlite3_finalize(lookup);
    }
  }

  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT INTO paths(id, site_id, map_id, from_position_id, to_position_id, auto_created, "
                         "points_json, created_at, updated_at) "
                         "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9) "
                         "ON CONFLICT(map_id, from_position_id, to_position_id) DO UPDATE SET "
                         "points_json=excluded.points_json, auto_created=excluded.auto_created, updated_at=excluded.updated_at",
                         -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, map_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 4, from_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 5, to_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_int(stmt, 6, auto_created ? 1 : 0);
  sqlite3_bind_text(stmt, 7, points_json.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);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  sqlite3_finalize(stmt);
  if (!ok)
  {
    err = sqlite3_errmsg(db_.handle());
    return std::nullopt;
  }

  sqlite3_stmt* get_stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), (std::string(kSelect) + " WHERE id = ?1").c_str(), -1, &get_stmt, nullptr) !=
      SQLITE_OK)
  {
    err = sqlite3_errmsg(db_.handle());
    return std::nullopt;
  }
  sqlite3_bind_text(get_stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  std::optional<nlohmann::json> out;
  if (sqlite3_step(get_stmt) == SQLITE_ROW)
    out = rowToJson(get_stmt);
  sqlite3_finalize(get_stmt);
  return out;
}

bool PathStore::remove(const std::string& id, std::string& err)
{
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM paths 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_step(stmt);
  const bool ok = sqlite3_changes(db_.handle()) > 0;
  sqlite3_finalize(stmt);
  if (!ok)
    err = "path not found";
  return ok;
}

int PathStore::removeForMap(const std::string& map_id)
{
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM paths WHERE map_id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
    return 0;
  sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_step(stmt);
  const int n = sqlite3_changes(db_.handle());
  sqlite3_finalize(stmt);
  return n;
}

int PathStore::removeForPosition(const std::string& map_id, const std::string& position_id)
{
  if (map_id.empty() || position_id.empty())
    return 0;
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "DELETE FROM paths WHERE map_id = ?1 AND (from_position_id = ?2 OR to_position_id = ?2)",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return 0;
  sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, position_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_step(stmt);
  const int n = sqlite3_changes(db_.handle());
  sqlite3_finalize(stmt);
  return n;
}

}  // namespace lm
