#include "storage/map_store.hpp"

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

#include <sqlite3.h>

namespace lm {

namespace {

constexpr const char* kBaseImageName = "map_base.png";

constexpr const char* kMapSelect =
    "SELECT id, name, description, site_id, created_by, created_by_user, created_by_group, "
    "width, height, resolution, origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
    "created_at, updated_at FROM maps";

nlohmann::json rowToJson(sqlite3_stmt* stmt)
{
  auto textOrNull = [&](int col) -> nlohmann::json {
    if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
      return nullptr;
    return nlohmann::json(reinterpret_cast<const char*>(sqlite3_column_text(stmt, col)));
  };
  auto realOrNull = [&](int col) -> nlohmann::json {
    if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
      return nullptr;
    return nlohmann::json(sqlite3_column_double(stmt, col));
  };

  nlohmann::json zones = nlohmann::json::array();
  if (sqlite3_column_type(stmt, 15) != SQLITE_NULL)
  {
    try
    {
      zones = nlohmann::json::parse(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 15)));
    }
    catch (...)
    {
      zones = nlohmann::json::array();
    }
  }

  return {{"id", textOrNull(0)},
          {"name", textOrNull(1)},
          {"description", textOrNull(2)},
          {"site_id", textOrNull(3)},
          {"created_by", textOrNull(4)},
          {"created_by_user", textOrNull(5)},
          {"created_by_group", textOrNull(6)},
          {"width", realOrNull(7)},
          {"height", realOrNull(8)},
          {"resolution", realOrNull(9)},
          {"origin_x", realOrNull(10)},
          {"origin_y", realOrNull(11)},
          {"origin_yaw", realOrNull(12)},
          {"image_file", textOrNull(13)},
          {"yaml_file", textOrNull(14)},
          {"zones", zones},
          {"created_at", textOrNull(16)},
          {"updated_at", textOrNull(17)}};
}

}  // namespace

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

std::filesystem::path MapStore::mapDir(const std::string& id) const
{
  return db_.mapsDir() / id;
}

nlohmann::json MapStore::list() const
{
  std::lock_guard<std::mutex> lock(mu_);
  nlohmann::json maps = nlohmann::json::array();
  std::string sql = std::string(kMapSelect) + " ORDER BY name";
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
    return maps;
  while (sqlite3_step(stmt) == SQLITE_ROW)
    maps.push_back(rowToJson(stmt));
  sqlite3_finalize(stmt);
  return maps;
}

std::optional<nlohmann::json> MapStore::find(const std::string& id) const
{
  std::lock_guard<std::mutex> lock(mu_);
  std::string sql = std::string(kMapSelect) + " 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> MapStore::create(const nlohmann::json& payload, std::string& err)
{
  if (!payload.is_object())
  {
    err = "payload must be an object";
    return std::nullopt;
  }
  const std::string name = StringUtil::trimCopy(payload.value("name", ""));
  if (name.empty())
  {
    err = "name is required";
    return std::nullopt;
  }

  const std::string id = payload.value("id", IdUtil::newId());
  const std::string now = IdUtil::nowIso8601();
  const std::string description = payload.value("description", "");
  const std::string site_id = payload.value("site_id", "");
  const std::string created_by = payload.value("created_by", "");
  const std::string created_by_user = payload.value("created_by_user", "");
  const std::string created_by_group = payload.value("created_by_group", "");
  const auto zones = payload.contains("zones") ? payload["zones"] : nlohmann::json::array();

  std::error_code ec;
  std::filesystem::create_directories(mapDir(id), ec);
  if (ec)
  {
    err = "failed to create map directory";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT INTO maps(id, name, description, site_id, created_by, created_by_user, "
                         "created_by_group, width, height, resolution, origin_x, origin_y, origin_yaw, image_file, "
                         "yaml_file, zones_json, created_at, updated_at) "
                         "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18)",
                         -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, name.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, description.c_str(), -1, SQLITE_TRANSIENT);
  if (site_id.empty())
    sqlite3_bind_null(stmt, 4);
  else
    sqlite3_bind_text(stmt, 4, site_id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 5, created_by.c_str(), -1, SQLITE_TRANSIENT);
  if (created_by_user.empty())
    sqlite3_bind_null(stmt, 6);
  else
    sqlite3_bind_text(stmt, 6, created_by_user.c_str(), -1, SQLITE_TRANSIENT);
  if (created_by_group.empty())
    sqlite3_bind_null(stmt, 7);
  else
    sqlite3_bind_text(stmt, 7, created_by_group.c_str(), -1, SQLITE_TRANSIENT);
  if (payload.contains("width") && payload["width"].is_number())
    sqlite3_bind_double(stmt, 8, payload["width"].get<double>());
  else
    sqlite3_bind_null(stmt, 8);
  if (payload.contains("height") && payload["height"].is_number())
    sqlite3_bind_double(stmt, 9, payload["height"].get<double>());
  else
    sqlite3_bind_null(stmt, 9);
  if (payload.contains("resolution") && payload["resolution"].is_number())
    sqlite3_bind_double(stmt, 10, payload["resolution"].get<double>());
  else
    sqlite3_bind_null(stmt, 10);
  sqlite3_bind_double(stmt, 11, payload.value("origin_x", 0.0));
  sqlite3_bind_double(stmt, 12, payload.value("origin_y", 0.0));
  sqlite3_bind_double(stmt, 13, payload.value("origin_yaw", 0.0));
  sqlite3_bind_null(stmt, 14);
  sqlite3_bind_null(stmt, 15);
  const std::string zones_str = zones.dump();
  sqlite3_bind_text(stmt, 16, zones_str.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 17, now.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 18, 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;
  created["id"] = id;
  created["name"] = name;
  created["description"] = description;
  created["site_id"] = site_id.empty() ? nullptr : nlohmann::json(site_id);
  created["created_by"] = created_by;
  if (!created_by_user.empty())
    created["created_by_user"] = created_by_user;
  if (!created_by_group.empty())
    created["created_by_group"] = created_by_group;
  if (payload.contains("width") && payload["width"].is_number())
    created["width"] = payload["width"];
  if (payload.contains("height") && payload["height"].is_number())
    created["height"] = payload["height"];
  if (payload.contains("resolution") && payload["resolution"].is_number())
    created["resolution"] = payload["resolution"];
  created["origin_x"] = payload.value("origin_x", 0.0);
  created["origin_y"] = payload.value("origin_y", 0.0);
  created["origin_yaw"] = payload.value("origin_yaw", 0.0);
  created["image_file"] = nullptr;
  created["yaml_file"] = nullptr;
  created["zones"] = zones;
  created["created_at"] = now;
  created["updated_at"] = now;
  return created;
}

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

  nlohmann::json merged = *existing;
  for (const char* key :
       {"name", "description", "site_id", "width", "height", "resolution", "origin_x", "origin_y", "origin_yaw"})
  {
    if (payload.contains(key))
      merged[key] = payload[key];
  }
  if (payload.contains("zones"))
  {
    if (!payload["zones"].is_array())
    {
      err = "zones must be an array";
      return false;
    }
    merged["zones"] = payload["zones"];
  }

  const std::string now = IdUtil::nowIso8601();
  const std::string zones_str = merged.value("zones", nlohmann::json::array()).dump();

  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "UPDATE maps SET name=?2, description=?3, site_id=?4, width=?5, height=?6, resolution=?7, "
                         "origin_x=?8, origin_y=?9, origin_yaw=?10, zones_json=?11, updated_at=?12 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, merged.value("name", "").c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, merged.value("description", "").c_str(), -1, SQLITE_TRANSIENT);
  const std::string site_id = merged.value("site_id", "");
  if (site_id.empty())
    sqlite3_bind_null(stmt, 4);
  else
    sqlite3_bind_text(stmt, 4, site_id.c_str(), -1, SQLITE_TRANSIENT);
  if (merged["width"].is_number())
    sqlite3_bind_double(stmt, 5, merged["width"].get<double>());
  else
    sqlite3_bind_null(stmt, 5);
  if (merged["height"].is_number())
    sqlite3_bind_double(stmt, 6, merged["height"].get<double>());
  else
    sqlite3_bind_null(stmt, 6);
  if (merged["resolution"].is_number())
    sqlite3_bind_double(stmt, 7, merged["resolution"].get<double>());
  else
    sqlite3_bind_null(stmt, 7);
  sqlite3_bind_double(stmt, 8, merged.value("origin_x", 0.0));
  sqlite3_bind_double(stmt, 9, merged.value("origin_y", 0.0));
  sqlite3_bind_double(stmt, 10, merged.value("origin_yaw", 0.0));
  sqlite3_bind_text(stmt, 11, zones_str.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 12, 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 MapStore::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 maps 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;
  sqlite3_finalize(stmt);
  if (!ok)
  {
    err = "map not found";
    return false;
  }

  std::error_code ec;
  std::filesystem::remove_all(mapDir(id), ec);
  return true;
}

std::optional<std::filesystem::path> MapStore::imagePath(const std::string& id) const
{
  const auto map = find(id);
  if (!map || !(*map)["image_file"].is_string())
    return std::nullopt;
  const auto path = mapDir(id) / map->value("image_file", "");
  if (!std::filesystem::exists(path))
    return std::nullopt;
  return path;
}

std::optional<std::filesystem::path> MapStore::baseImagePath(const std::string& id) const
{
  const auto base = mapDir(id) / kBaseImageName;
  if (std::filesystem::exists(base))
    return base;
  return imagePath(id);
}

std::optional<std::filesystem::path> MapStore::yamlPath(const std::string& id) const
{
  const auto map = find(id);
  if (!map || !(*map)["yaml_file"].is_string())
    return std::nullopt;
  const auto path = mapDir(id) / map->value("yaml_file", "");
  if (!std::filesystem::exists(path))
    return std::nullopt;
  return path;
}

bool MapStore::saveImageFile(const std::string& id,
                             const std::string& filename,
                             const std::string& bytes,
                             std::string& err)
{
  if (!find(id))
  {
    err = "map not found";
    return false;
  }

  std::error_code ec;
  std::filesystem::create_directories(mapDir(id), ec);
  const auto path = mapDir(id) / filename;
  if (!FileUtil::writeBinaryAtomic(path, bytes))
  {
    err = "failed to write image file";
    return false;
  }

  const auto base_path = mapDir(id) / kBaseImageName;
  if (!FileUtil::writeBinaryAtomic(base_path, bytes))
  {
    err = "failed to write base image file";
    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 maps SET image_file = ?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_text(stmt, 2, filename.c_str(), -1, SQLITE_TRANSIENT);
  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;
}

bool MapStore::saveCompositeImageFile(const std::string& id, const std::string& bytes, std::string& err)
{
  const auto map = find(id);
  if (!map)
  {
    err = "map not found";
    return false;
  }
  const std::string filename = map->value("image_file", "map.png");
  if (filename.empty())
  {
    err = "map has no image file";
    return false;
  }

  std::error_code ec;
  std::filesystem::create_directories(mapDir(id), ec);
  const auto path = mapDir(id) / filename;
  if (!FileUtil::writeBinaryAtomic(path, bytes))
  {
    err = "failed to write composite image file";
    return false;
  }

  const auto base_path = mapDir(id) / kBaseImageName;
  if (!std::filesystem::exists(base_path))
  {
    if (!FileUtil::writeBinaryAtomic(base_path, bytes))
    {
      err = "failed to initialize base image file";
      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 maps SET updated_at = ?2 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, 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 MapStore::saveBaseImageFile(const std::string& id, const std::string& bytes, std::string& err)
{
  if (!find(id))
  {
    err = "map not found";
    return false;
  }

  std::error_code ec;
  std::filesystem::create_directories(mapDir(id), ec);
  const auto path = mapDir(id) / kBaseImageName;
  if (!FileUtil::writeBinaryAtomic(path, bytes))
  {
    err = "failed to write base image file";
    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 maps SET updated_at = ?2 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, 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 MapStore::saveYamlFile(const std::string& id, const std::string& yaml_text, std::string& err)
{
  if (!find(id))
  {
    err = "map not found";
    return false;
  }

  constexpr const char* kYamlName = "map.yaml";
  std::error_code ec;
  std::filesystem::create_directories(mapDir(id), ec);
  const auto path = mapDir(id) / kYamlName;
  if (!FileUtil::writeBinaryAtomic(path, yaml_text))
  {
    err = "failed to write yaml file";
    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 maps SET yaml_file = ?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_text(stmt, 2, kYamlName, -1, SQLITE_TRANSIENT);
  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
