#include "storage/database.hpp"

#include "util/file_util.hpp"
#include "util/id_util.hpp"

#include <sqlite3.h>

#include <cstdio>

namespace lm {

namespace {

bool execSql(sqlite3* db, const char* sql, std::string& err)
{
  char* msg = nullptr;
  const int rc = sqlite3_exec(db, sql, nullptr, nullptr, &msg);
  if (rc != SQLITE_OK)
  {
    err = msg ? msg : sqlite3_errstr(rc);
    sqlite3_free(msg);
    return false;
  }
  return true;
}

const char* kSchemaSql = R"SQL(
CREATE TABLE IF NOT EXISTS meta (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS documents (
  name TEXT PRIMARY KEY,
  content TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS layout_profiles (
  id TEXT PRIMARY KEY,
  content TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS sites (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS maps (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  site_id TEXT,
  created_by TEXT NOT NULL DEFAULT '',
  created_by_user TEXT,
  created_by_group TEXT,
  width REAL,
  height REAL,
  resolution REAL,
  origin_x REAL DEFAULT 0,
  origin_y REAL DEFAULT 0,
  origin_yaw REAL DEFAULT 0,
  image_file TEXT,
  yaml_file TEXT,
  zones_json TEXT NOT NULL DEFAULT '[]',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS sounds (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  file_name TEXT,
  duration_ms INTEGER,
  enabled INTEGER NOT NULL DEFAULT 1,
  volume INTEGER NOT NULL DEFAULT 100,
  is_system INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS recordings (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL DEFAULT '',
  map_id TEXT,
  file_path TEXT,
  started_at TEXT,
  ended_at TEXT,
  created_at TEXT NOT NULL,
  FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS transitions (
  id TEXT PRIMARY KEY,
  site_id TEXT NOT NULL,
  from_map_id TEXT NOT NULL,
  to_map_id TEXT NOT NULL,
  start_position_id TEXT NOT NULL,
  goal_position_id TEXT NOT NULL,
  mission_id TEXT NOT NULL,
  created_by TEXT NOT NULL DEFAULT '',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS io_modules (
  id TEXT PRIMARY KEY,
  site_id TEXT NOT NULL,
  name TEXT NOT NULL,
  module_type TEXT NOT NULL,
  ip_address TEXT NOT NULL,
  created_by TEXT NOT NULL DEFAULT '',
  created_by_group TEXT NOT NULL DEFAULT '',
  connected INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS paths (
  id TEXT PRIMARY KEY,
  site_id TEXT NOT NULL,
  map_id TEXT NOT NULL,
  from_position_id TEXT NOT NULL,
  to_position_id TEXT NOT NULL,
  auto_created INTEGER NOT NULL DEFAULT 1,
  points_json TEXT NOT NULL DEFAULT '[]',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  UNIQUE(map_id, from_position_id, to_position_id),
  FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
  FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS path_guides (
  id TEXT PRIMARY KEY,
  site_id TEXT NOT NULL,
  map_id TEXT NOT NULL,
  name TEXT NOT NULL,
  positions_json TEXT NOT NULL DEFAULT '[]',
  created_by TEXT NOT NULL DEFAULT '',
  created_by_group TEXT NOT NULL DEFAULT '',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
  FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS dashboards (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  created_by TEXT NOT NULL DEFAULT '',
  created_by_user TEXT,
  is_default INTEGER NOT NULL DEFAULT 0,
  sort_order INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS dashboard_edit_groups (
  dashboard_id TEXT NOT NULL,
  group_id TEXT NOT NULL,
  PRIMARY KEY (dashboard_id, group_id),
  FOREIGN KEY (dashboard_id) REFERENCES dashboards(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS dashboard_widgets (
  id TEXT PRIMARY KEY,
  dashboard_id TEXT NOT NULL,
  type TEXT NOT NULL,
  title TEXT NOT NULL DEFAULT '',
  mission_id TEXT,
  mission_group TEXT,
  config_json TEXT NOT NULL DEFAULT '{}',
  sort_order INTEGER NOT NULL DEFAULT 0,
  FOREIGN KEY (dashboard_id) REFERENCES dashboards(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS dashboard_state (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  active_dashboard_id TEXT
);
)SQL";

}  // namespace

Database::Database(std::filesystem::path data_dir)
    : data_dir_(std::move(data_dir)), db_path_(data_dir_ / "RBS.db")
{
}

void Database::close()
{
  if (db_)
  {
    sqlite3_close(db_);
    db_ = nullptr;
  }
}

bool Database::openDb(std::string& err)
{
  std::error_code ec;
  std::filesystem::create_directories(data_dir_, ec);

  const auto legacy_path = data_dir_ / "test3.db";
  if (!std::filesystem::exists(db_path_) && std::filesystem::exists(legacy_path))
  {
    std::filesystem::rename(legacy_path, db_path_, ec);
    for (const char* suffix : {"-wal", "-shm"})
    {
      const auto from = legacy_path.string() + suffix;
      const auto to = db_path_.string() + suffix;
      if (std::filesystem::exists(from))
        std::filesystem::rename(from, to, ec);
    }
  }

  const int rc = sqlite3_open(db_path_.string().c_str(), &db_);
  if (rc != SQLITE_OK)
  {
    err = sqlite3_errmsg(db_);
    db_ = nullptr;
    return false;
  }

  sqlite3_busy_timeout(db_, 5000);
  if (!execSql(db_, "PRAGMA journal_mode=WAL;", err))
    return false;
  if (!execSql(db_, "PRAGMA synchronous=NORMAL;", err))
    return false;
  if (!execSql(db_, "PRAGMA foreign_keys=ON;", err))
    return false;
  return true;
}

bool Database::ensureDataDirs(std::string& err)
{
  std::error_code ec;
  for (const auto& dir : {mapsDir(), soundsDir(), recordingsDir()})
  {
    if (!std::filesystem::create_directories(dir, ec) && ec)
    {
      err = "failed to create directory: " + dir.string();
      return false;
    }
  }
  return true;
}

bool Database::migrateLegacyMapsDir(std::string& err)
{
  if (data_dir_.filename() != "data")
    return true;

  const std::filesystem::path legacy = "maps";
  const auto target = mapsDir();
  if (!std::filesystem::is_directory(legacy) || legacy == target)
    return true;

  std::error_code ec;
  std::filesystem::create_directories(target, ec);
  if (ec)
  {
    err = "failed to create maps directory: " + target.string();
    return false;
  }

  for (const auto& entry : std::filesystem::directory_iterator(legacy, ec))
  {
    if (ec || !entry.is_directory())
      continue;
    const auto dest = target / entry.path().filename();
    if (std::filesystem::exists(dest))
      continue;
    std::filesystem::rename(entry.path(), dest, ec);
    if (ec)
    {
      ec.clear();
      std::filesystem::copy(entry.path(), dest, std::filesystem::copy_options::recursive, ec);
      if (ec)
      {
        err = "failed to migrate map directory: " + entry.path().string();
        return false;
      }
      std::filesystem::remove_all(entry.path(), ec);
      ec.clear();
    }
  }
  return true;
}

bool Database::applySchema(std::string& err)
{
  return execSql(db_, kSchemaSql, err);
}

namespace {

bool tableHasColumn(sqlite3* db, const char* table, const char* column)
{
  std::string sql = "PRAGMA table_info(";
  sql += table;
  sql += ")";
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
    return false;
  bool found = false;
  while (sqlite3_step(stmt) == SQLITE_ROW)
  {
    const char* name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
    if (name && column == std::string(name))
    {
      found = true;
      break;
    }
  }
  sqlite3_finalize(stmt);
  return found;
}

}  // namespace

bool Database::applySchemaMigrations(std::string& err)
{
  std::string ver = getMeta("schema_version").value_or("1");

  if (ver == "1")
  {
    if (!execSql(db_,
                 "CREATE TABLE IF NOT EXISTS sites ("
                 "id TEXT PRIMARY KEY, name TEXT NOT NULL, "
                 "created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
                 err))
      return false;

    if (!tableHasColumn(db_, "maps", "site_id"))
    {
      if (!execSql(db_, "ALTER TABLE maps ADD COLUMN site_id TEXT", err))
        return false;
    }
    if (!tableHasColumn(db_, "maps", "created_by"))
    {
      if (!execSql(db_, "ALTER TABLE maps ADD COLUMN created_by TEXT NOT NULL DEFAULT ''", err))
        return false;
    }

    const std::string now = IdUtil::nowIso8601();
    sqlite3_stmt* stmt = nullptr;
    if (sqlite3_prepare_v2(db_,
                           "INSERT OR IGNORE INTO sites(id, name, created_at, updated_at) "
                           "VALUES('site_configuration', 'ConfigurationSite', ?1, ?1)",
                           -1,
                           &stmt,
                           nullptr) == SQLITE_OK)
    {
      sqlite3_bind_text(stmt, 1, now.c_str(), -1, SQLITE_TRANSIENT);
      sqlite3_step(stmt);
      sqlite3_finalize(stmt);
    }

    if (sqlite3_prepare_v2(db_,
                           "UPDATE maps SET site_id = 'site_configuration' WHERE site_id IS NULL OR site_id = ''",
                           -1,
                           &stmt,
                           nullptr) == SQLITE_OK)
    {
      sqlite3_step(stmt);
      sqlite3_finalize(stmt);
    }

    setMeta("schema_version", "2");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "2")
  {
    if (!tableHasColumn(db_, "maps", "created_by_user"))
    {
      if (!execSql(db_, "ALTER TABLE maps ADD COLUMN created_by_user TEXT", err))
        return false;
    }
    if (!tableHasColumn(db_, "maps", "created_by_group"))
    {
      if (!execSql(db_, "ALTER TABLE maps ADD COLUMN created_by_group TEXT", err))
        return false;
    }
    setMeta("schema_version", "3");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "3")
  {
    if (!tableHasColumn(db_, "sounds", "volume"))
    {
      if (!execSql(db_, "ALTER TABLE sounds ADD COLUMN volume INTEGER NOT NULL DEFAULT 100", err))
        return false;
    }
    if (!tableHasColumn(db_, "sounds", "is_system"))
    {
      if (!execSql(db_, "ALTER TABLE sounds ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0", err))
        return false;
    }
    setMeta("schema_version", "4");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "4")
  {
    if (!execSql(db_,
                 "CREATE TABLE IF NOT EXISTS transitions ("
                 "id TEXT PRIMARY KEY, "
                 "site_id TEXT NOT NULL, "
                 "from_map_id TEXT NOT NULL, "
                 "to_map_id TEXT NOT NULL, "
                 "start_position_id TEXT NOT NULL, "
                 "goal_position_id TEXT NOT NULL, "
                 "mission_id TEXT NOT NULL, "
                 "created_by TEXT NOT NULL DEFAULT '', "
                 "created_at TEXT NOT NULL, "
                 "updated_at TEXT NOT NULL"
                 ")",
                 err))
      return false;
    setMeta("schema_version", "5");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "5")
  {
    if (!tableHasColumn(db_, "transitions", "created_by"))
    {
      if (!execSql(db_, "ALTER TABLE transitions ADD COLUMN created_by TEXT NOT NULL DEFAULT ''", err))
        return false;
    }
    setMeta("schema_version", "6");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "6")
  {
    if (!execSql(db_,
                 "CREATE TABLE IF NOT EXISTS io_modules ("
                 "id TEXT PRIMARY KEY, "
                 "site_id TEXT NOT NULL, "
                 "name TEXT NOT NULL, "
                 "module_type TEXT NOT NULL, "
                 "ip_address TEXT NOT NULL, "
                 "created_by TEXT NOT NULL DEFAULT '', "
                 "created_by_group TEXT NOT NULL DEFAULT '', "
                 "connected INTEGER NOT NULL DEFAULT 0, "
                 "created_at TEXT NOT NULL, "
                 "updated_at TEXT NOT NULL, "
                 "FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE"
                 ")",
                 err))
      return false;
    setMeta("schema_version", "7");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "7")
  {
    if (!execSql(db_,
                 "CREATE TABLE IF NOT EXISTS paths ("
                 "id TEXT PRIMARY KEY, "
                 "site_id TEXT NOT NULL, "
                 "map_id TEXT NOT NULL, "
                 "from_position_id TEXT NOT NULL, "
                 "to_position_id TEXT NOT NULL, "
                 "auto_created INTEGER NOT NULL DEFAULT 1, "
                 "points_json TEXT NOT NULL DEFAULT '[]', "
                 "created_at TEXT NOT NULL, "
                 "updated_at TEXT NOT NULL, "
                 "UNIQUE(map_id, from_position_id, to_position_id), "
                 "FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE, "
                 "FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE"
                 ")",
                 err))
      return false;
    setMeta("schema_version", "8");
  }

  ver = getMeta("schema_version").value_or("1");
  if (ver == "8")
  {
    if (!execSql(db_,
                 "CREATE TABLE IF NOT EXISTS path_guides ("
                 "id TEXT PRIMARY KEY, "
                 "site_id TEXT NOT NULL, "
                 "map_id TEXT NOT NULL, "
                 "name TEXT NOT NULL, "
                 "positions_json TEXT NOT NULL DEFAULT '[]', "
                 "created_by TEXT NOT NULL DEFAULT '', "
                 "created_by_group TEXT NOT NULL DEFAULT '', "
                 "created_at TEXT NOT NULL, "
                 "updated_at TEXT NOT NULL, "
                 "FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE, "
                 "FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE"
                 ")",
                 err))
      return false;
    setMeta("schema_version", "9");
  }

  return true;
}

std::optional<std::string> Database::getMeta(const std::string& key) const
{
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_, "SELECT value FROM meta WHERE key = ?1", -1, &stmt, nullptr) != SQLITE_OK)
    return std::nullopt;
  sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
  std::optional<std::string> out;
  if (sqlite3_step(stmt) == SQLITE_ROW)
  {
    const char* val = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
    if (val)
      out = val;
  }
  sqlite3_finalize(stmt);
  return out;
}

bool Database::setMeta(const std::string& key, const std::string& value)
{
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_,
                         "INSERT INTO meta(key, value) VALUES(?1, ?2) "
                         "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, value.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  sqlite3_finalize(stmt);
  return ok;
}

bool Database::getDocument(const std::string& name, nlohmann::json& out) const
{
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_, "SELECT content FROM documents WHERE name = ?1", -1, &stmt, nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
  bool found = false;
  if (sqlite3_step(stmt) == SQLITE_ROW)
  {
    const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
    if (text)
    {
      try
      {
        out = nlohmann::json::parse(text);
        found = true;
      }
      catch (...)
      {
        found = false;
      }
    }
  }
  sqlite3_finalize(stmt);
  return found;
}

bool Database::setDocument(const std::string& name, const nlohmann::json& doc)
{
  std::lock_guard<std::mutex> lock(mu_);
  const std::string now = IdUtil::nowIso8601();
  const std::string body = doc.dump();
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_,
                         "INSERT INTO documents(name, content, updated_at) VALUES(?1, ?2, ?3) "
                         "ON CONFLICT(name) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, body.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  sqlite3_finalize(stmt);
  return ok;
}

std::optional<nlohmann::json> Database::getLayoutProfile(const std::string& id) const
{
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_, "SELECT content FROM layout_profiles WHERE id = ?1", -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)
  {
    const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
    if (text)
    {
      try
      {
        out = nlohmann::json::parse(text);
      }
      catch (...)
      {
        out = std::nullopt;
      }
    }
  }
  sqlite3_finalize(stmt);
  return out;
}

bool Database::setLayoutProfile(const nlohmann::json& profile)
{
  if (!profile.is_object() || !profile.contains("id") || !profile["id"].is_string())
    return false;
  const std::string id = profile["id"].get<std::string>();
  const std::string now = IdUtil::nowIso8601();
  const std::string body = profile.dump();

  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_,
                         "INSERT INTO layout_profiles(id, content, updated_at) VALUES(?1, ?2, ?3) "
                         "ON CONFLICT(id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 2, body.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  sqlite3_finalize(stmt);
  return ok;
}

bool Database::deleteLayoutProfile(const std::string& id)
{
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_, "DELETE FROM layout_profiles WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
  const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
  sqlite3_finalize(stmt);
  return ok;
}

bool Database::migrateFromJsonIfNeeded(std::string& err)
{
  if (getMeta("json_imported"))
    return true;

  const auto importDoc = [&](const std::string& name, const std::filesystem::path& path) {
    if (!std::filesystem::exists(path))
      return;
    try
    {
      const auto doc = nlohmann::json::parse(FileUtil::readBinary(path));
      setDocument(name, doc);
    }
    catch (...)
    {
    }
  };

  importDoc("auth", data_dir_ / "auth.json");
  importDoc("missions", data_dir_ / "missions.json");
  importDoc("mission_queue", data_dir_ / "mission_queue.json");
  importDoc("robot_runtime", data_dir_ / "robot_runtime.json");

  const auto state_path = data_dir_ / "state.json";
  if (std::filesystem::exists(state_path))
  {
    try
    {
      setDocument("state", nlohmann::json::parse(FileUtil::readBinary(state_path)));
    }
    catch (...)
    {
    }
  }

  const auto models_dir = data_dir_ / "models";
  if (std::filesystem::is_directory(models_dir))
  {
    for (const auto& entry : std::filesystem::directory_iterator(models_dir))
    {
      if (!entry.is_regular_file() || entry.path().extension() != ".json")
        continue;
      try
      {
        const auto profile = nlohmann::json::parse(FileUtil::readBinary(entry.path()));
        setLayoutProfile(profile);
      }
      catch (...)
      {
      }
    }
  }

  setMeta("schema_version", "1");
  setMeta("json_imported", IdUtil::nowIso8601());
  std::fprintf(stderr, "SQLite: imported JSON data into %s\n", db_path_.string().c_str());
  return true;
}

bool Database::init(std::string& err)
{
  if (!openDb(err))
    return false;
  if (!applySchema(err))
    return false;
  if (!applySchemaMigrations(err))
    return false;
  if (!ensureDataDirs(err))
    return false;
  if (!migrateLegacyMapsDir(err))
    return false;
  if (!migrateFromJsonIfNeeded(err))
    return false;
  if (!getMeta("schema_version"))
    setMeta("schema_version", "1");
  return true;
}

}  // namespace lm
