#include "storage/path_guide_store.hpp"

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

#include <sqlite3.h>

#include <algorithm>

namespace lm {

namespace {

nlohmann::json parsePositionsJson(const std::string& raw)
{
  if (raw.empty())
    return nlohmann::json::array();
  try
  {
    const auto parsed = nlohmann::json::parse(raw);
    return parsed.is_array() ? parsed : nlohmann::json::array();
  }
  catch (...)
  {
    return nlohmann::json::array();
  }
}

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) : "";
  };

  const nlohmann::json positions = parsePositionsJson(text(4));
  int starts_count = 0;
  int vias_count = 0;
  int goals_count = 0;
  for (const auto& pos : positions)
  {
    if (!pos.is_object())
      continue;
    const std::string role = StringUtil::toLower(pos.value("role", ""));
    if (role == "start")
      ++starts_count;
    else if (role == "via")
      ++vias_count;
    else if (role == "goal")
      ++goals_count;
  }

  return {{"id", text(0)},
          {"site_id", text(1)},
          {"map_id", text(2)},
          {"name", text(3)},
          {"positions", positions},
          {"starts_count", starts_count},
          {"vias_count", vias_count},
          {"goals_count", goals_count},
          {"created_by", text(5)},
          {"created_by_group", text(6)},
          {"created_at", text(7)},
          {"updated_at", text(8)}};
}

constexpr const char* kSelect =
    "SELECT id, site_id, map_id, name, positions_json, created_by, created_by_group, created_at, updated_at "
    "FROM path_guides";

bool validRole(const std::string& role)
{
  return role == "start" || role == "via" || role == "goal";
}

std::optional<nlohmann::json> normalizePositions(const nlohmann::json& raw, std::string& err)
{
  if (!raw.is_array())
  {
    err = "positions must be an array";
    return std::nullopt;
  }

  nlohmann::json out = nlohmann::json::array();
  int starts = 0;
  int goals = 0;
  int via_priority = 0;

  for (const auto& item : raw)
  {
    if (!item.is_object())
      continue;
    const std::string position_id = StringUtil::trimCopy(item.value("position_id", ""));
    const std::string role = StringUtil::toLower(StringUtil::trimCopy(item.value("role", "")));
    if (position_id.empty() || !validRole(role))
    {
      err = "invalid position entry";
      return std::nullopt;
    }

    nlohmann::json entry = {{"position_id", position_id}, {"role", role}};
    if (role == "via")
    {
      int priority = via_priority + 1;
      if (item.contains("priority") && item["priority"].is_number_integer())
        priority = item["priority"].get<int>();
      if (priority <= 0)
        priority = via_priority + 1;
      via_priority = priority;
      entry["priority"] = priority;
    }
    else if (role == "start")
    {
      ++starts;
    }
    else if (role == "goal")
    {
      ++goals;
    }
    out.push_back(std::move(entry));
  }

  if (starts < 1 || goals < 1)
  {
    err = "path guide requires at least one start and one goal";
    return std::nullopt;
  }

  std::sort(out.begin(), out.end(), [](const nlohmann::json& a, const nlohmann::json& b) {
    const std::string ra = a.value("role", "");
    const std::string rb = b.value("role", "");
    if (ra != rb)
    {
      if (ra == "start")
        return true;
      if (rb == "start")
        return false;
      if (ra == "via")
        return true;
      return false;
    }
    if (ra == "via")
      return a.value("priority", 0) < b.value("priority", 0);
    return a.value("position_id", "") < b.value("position_id", "");
  });

  return out;
}

}  // namespace

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

bool PathGuideStore::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 path_guides WHERE site_id = ?1 AND lower(name) = lower(?2) LIMIT 1"
                        : "SELECT id FROM path_guides 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 PathGuideStore::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 site_id, map_id, 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> PathGuideStore::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> PathGuideStore::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 map_id = StringUtil::trimCopy(payload.value("map_id", ""));
  const std::string name = StringUtil::trimCopy(payload.value("name", ""));
  const std::string created_by = StringUtil::trimCopy(payload.value("created_by", ""));
  const std::string created_by_group = StringUtil::trimCopy(payload.value("created_by_group", ""));
  const auto positions = normalizePositions(payload.value("positions", nlohmann::json::array()), err);
  if (!positions)
    return std::nullopt;

  if (site_id.empty() || map_id.empty() || name.empty())
  {
    err = "missing required fields";
    return std::nullopt;
  }

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

  const std::string id = payload.value("id", IdUtil::newId());
  const std::string now = IdUtil::nowIso8601();
  const std::string positions_json = positions->dump();

  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT INTO path_guides(id, site_id, map_id, name, positions_json, created_by, "
                         "created_by_group, created_at, updated_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?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, map_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 4, name.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 5, positions_json.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);

  nlohmann::json created = {{"id", id},
                            {"site_id", site_id},
                            {"map_id", map_id},
                            {"name", name},
                            {"positions", *positions},
                            {"created_by", created_by},
                            {"created_by_group", created_by_group},
                            {"created_at", now},
                            {"updated_at", now}};
  int starts_count = 0;
  int vias_count = 0;
  int goals_count = 0;
  for (const auto& pos : *positions)
  {
    const std::string role = pos.value("role", "");
    if (role == "start")
      ++starts_count;
    else if (role == "via")
      ++vias_count;
    else if (role == "goal")
      ++goals_count;
  }
  created["starts_count"] = starts_count;
  created["vias_count"] = vias_count;
  created["goals_count"] = goals_count;
  return created;
}

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

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

  const std::string site_id = StringUtil::trimCopy(merged.value("site_id", ""));
  const std::string map_id = StringUtil::trimCopy(merged.value("map_id", ""));
  const std::string name = StringUtil::trimCopy(merged.value("name", ""));
  const auto positions = normalizePositions(merged.value("positions", nlohmann::json::array()), err);
  if (!positions)
    return false;

  if (site_id.empty() || map_id.empty() || name.empty())
  {
    err = "missing required fields";
    return false;
  }

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

  const std::string now = IdUtil::nowIso8601();
  const std::string positions_json = positions->dump();

  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "UPDATE path_guides SET site_id=?2, map_id=?3, name=?4, positions_json=?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, map_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 4, name.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 5, positions_json.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 PathGuideStore::remove(const std::string& id, std::string& err)
{
  if (!find(id))
  {
    err = "path guide not found";
    return false;
  }
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM path_guides 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;
}

}  // namespace lm
