#include "auth/auth_service.hpp"

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

#include <algorithm>
#include <cstdlib>
#include <ctime>

namespace lm {

thread_local const AuthSession* AuthService::tls_session_ = nullptr;

namespace {

constexpr const char* kSessionCookie = "lm_session";

nlohmann::json defaultPermissionsAllWrite()
{
  return {{"dashboard", "write"},
          {"config", "write"},
          {"maps", "write"},
          {"missions", "write"},
          {"sounds", "write"},
          {"integrations", "write"},
          {"users", "write"}};
}

nlohmann::json defaultPermissionsAdministrator()
{
  return {{"dashboard", "write"},
          {"config", "none"},
          {"maps", "write"},
          {"missions", "write"},
          {"sounds", "write"},
          {"integrations", "write"},
          {"users", "write"}};
}

nlohmann::json defaultPermissionsUserGroup()
{
  return {{"dashboard", "write"},
          {"config", "none"},
          {"maps", "none"},
          {"missions", "read"},
          {"sounds", "read"},
          {"integrations", "read"},
          {"users", "none"}};
}

nlohmann::json makeUser(const std::string& id,
                        const std::string& username,
                        const std::string& password,
                        const std::string& group_id,
                        const std::string& display_name)
{
  const std::string salt = CryptoUtil::randomToken(16);
  return {{"id", id},
          {"username", username},
          {"display_name", display_name},
          {"group_id", group_id},
          {"password_salt", salt},
          {"password_hash", CryptoUtil::hashPassword(salt, password)},
          {"pin_salt", nullptr},
          {"pin_hash", nullptr},
          {"enabled", true}};
}

}  // namespace

AuthService::AuthService(Database& db) : db_(db)
{
  loadOrSeed();
}

void AuthService::loadOrSeed()
{
  std::lock_guard<std::mutex> lock(mu_);
  data_ = nlohmann::json::object();
  if (!db_.getDocument("auth", data_))
    data_ = nlohmann::json::object();

  if (!data_.contains("version"))
    data_["version"] = 1;
  if (!data_.contains("groups") || !data_["groups"].is_array() || data_["groups"].empty())
  {
    data_["groups"] = nlohmann::json::array({
        {{"id", "group_distributors"},
         {"name", "Distributors"},
         {"allow_pin", false},
         {"permissions", defaultPermissionsAllWrite()}},
        {{"id", "group_administrators"},
         {"name", "Administrators"},
         {"allow_pin", false},
         {"permissions", defaultPermissionsAdministrator()}},
        {{"id", "group_users"},
         {"name", "Users"},
         {"allow_pin", true},
         {"permissions", defaultPermissionsUserGroup()}},
    });
  }
  if (!data_.contains("users") || !data_["users"].is_array() || data_["users"].empty())
  {
    data_["users"] = nlohmann::json::array({
        makeUser("user_distributor", "Distributor", "distributor", "group_distributors", "Distributor"),
        makeUser("user_admin", "Admin", "admin", "group_administrators", "Administrator"),
        makeUser("user_operator", "User", "user", "group_users", "Operator"),
    });
  }

  if (data_.value("version", 1) < 2)
  {
    if (data_.contains("groups") && data_["groups"].is_array())
    {
      for (auto& g : data_["groups"])
      {
        if (!g.contains("permissions") || !g["permissions"].is_object())
          g["permissions"] = nlohmann::json::object();
        const std::string id = g.value("id", "");
        if (id == "group_distributors")
          g["permissions"]["config"] = "write";
        else
          g["permissions"]["config"] = "none";
      }
    }
    data_["version"] = 2;
  }

  if (data_.value("version", 1) < 3)
  {
    if (data_.contains("groups") && data_["groups"].is_array())
    {
      for (auto& g : data_["groups"])
      {
        if (!g.contains("permissions") || !g["permissions"].is_object())
          g["permissions"] = nlohmann::json::object();
        const std::string id = g.value("id", "");
        if (id == "group_distributors" || id == "group_administrators")
          g["permissions"]["maps"] = "write";
        else
          g["permissions"]["maps"] = "none";
      }
    }
    data_["version"] = 3;
  }

  if (data_.value("version", 1) < 4)
  {
    if (data_.contains("groups") && data_["groups"].is_array())
    {
      for (auto& g : data_["groups"])
      {
        if (!g.contains("permissions") || !g["permissions"].is_object())
          continue;
        auto& perms = g["permissions"];
        if (!perms.contains("sounds"))
          perms["sounds"] = perms.value("integrations", "none");
      }
    }
    data_["version"] = 4;
  }

  saveUnlocked();
}

void AuthService::saveUnlocked()
{
  db_.setDocument("auth", data_);
}

bool AuthService::canDeleteMap(const nlohmann::json& map, const AuthSession& session)
{
  if (map.contains("created_by_group") && map["created_by_group"].is_string())
  {
    const std::string map_group = map["created_by_group"].get<std::string>();
    if (!map_group.empty())
      return map_group == session.group_id;
  }
  return true;
}

std::string AuthService::extractToken(const httplib::Request& req) const
{
  if (req.has_header("Cookie"))
  {
    const std::string cookie = req.get_header_value("Cookie");
    const std::string prefix = std::string(kSessionCookie) + "=";
    const auto pos = cookie.find(prefix);
    if (pos != std::string::npos)
    {
      const auto start = pos + prefix.size();
      const auto end = cookie.find(';', start);
      return cookie.substr(start, end == std::string::npos ? std::string::npos : end - start);
    }
  }
  if (req.has_header("Authorization"))
  {
    const std::string auth = req.get_header_value("Authorization");
    constexpr const char* kBearer = "Bearer ";
    if (auth.rfind(kBearer, 0) == 0)
      return auth.substr(std::strlen(kBearer));
  }
  return {};
}

bool AuthService::isPublicApiPath(const std::string& path, const std::string& method)
{
  if (method == "OPTIONS")
    return true;
  return path == "/api/health" || path == "/api/auth/login" || path == "/api/auth/logout" ||
         path == "/api/auth/me";
}

std::optional<std::string> AuthService::resourceForApiPath(const std::string& path)
{
  if (path.rfind("/api/auth/", 0) == 0)
    return std::nullopt;
  if (path.rfind("/api/users", 0) == 0 || path.rfind("/api/user_groups", 0) == 0)
    return "users";
  if (path.rfind("/api/missions", 0) == 0 || path.rfind("/api/mission_queue", 0) == 0 ||
      path.rfind("/api/robot", 0) == 0)
    return "missions";
  if (path.rfind("/api/triggers", 0) == 0 || path.rfind("/api/schedules", 0) == 0 ||
      path.rfind("/api/robots", 0) == 0 || path.rfind("/api/fleet", 0) == 0 ||
      path.rfind("/api/modbus", 0) == 0 || path.rfind("/api/v2.0.0/", 0) == 0)
    return "integrations";
  if (path.rfind("/api/dashboards", 0) == 0)
    return "dashboard";
  if (path.rfind("/api/sounds", 0) == 0)
    return "sounds";
  if (path.rfind("/api/io_modules", 0) == 0)
    return "integrations";
  if (path == "/api/robot/active_map")
    return "maps";
  if (path.rfind("/api/maps", 0) == 0 || path.rfind("/api/sites", 0) == 0 ||
      path.rfind("/api/paths", 0) == 0 || path.rfind("/api/transitions", 0) == 0 ||
      path.rfind("/api/recordings", 0) == 0)
    return "maps";
  if (path.rfind("/api/layout", 0) == 0 || path.rfind("/api/lidars", 0) == 0 ||
      path.rfind("/api/imus", 0) == 0)
    return "config";
  return std::nullopt;
}

bool AuthService::requiresWrite(const std::string& method)
{
  return method != "GET" && method != "HEAD" && method != "OPTIONS";
}

bool AuthService::permissionAllows(const nlohmann::json& perms,
                                   const std::string& resource,
                                   bool write) const
{
  if (!perms.is_object() || !perms.contains(resource))
    return false;
  const std::string level = perms.value(resource, "none");
  if (level == "none")
    return false;
  if (write)
    return level == "write";
  return level == "read" || level == "write";
}

const nlohmann::json* AuthService::findUserByIdUnlocked(const std::string& id) const
{
  if (!data_.contains("users") || !data_["users"].is_array())
    return nullptr;
  for (const auto& u : data_["users"])
  {
    if (u.value("id", "") == id)
      return &u;
  }
  return nullptr;
}

const nlohmann::json* AuthService::findUserByUsernameUnlocked(const std::string& username) const
{
  if (!data_.contains("users") || !data_["users"].is_array())
    return nullptr;
  const std::string needle = StringUtil::toLower(StringUtil::trimCopy(username));
  for (const auto& u : data_["users"])
  {
    if (StringUtil::toLower(u.value("username", "")) == needle)
      return &u;
  }
  return nullptr;
}

const nlohmann::json* AuthService::findGroupByIdUnlocked(const std::string& id) const
{
  if (!data_.contains("groups") || !data_["groups"].is_array())
    return nullptr;
  for (const auto& g : data_["groups"])
  {
    if (g.value("id", "") == id)
      return &g;
  }
  return nullptr;
}

bool AuthService::verifyPasswordUnlocked(const nlohmann::json& user, const std::string& password) const
{
  const std::string salt = user.value("password_salt", "");
  const std::string hash = user.value("password_hash", "");
  if (salt.empty() || hash.empty())
    return false;
  return CryptoUtil::hashPassword(salt, password) == hash;
}

bool AuthService::verifyPinUnlocked(const nlohmann::json& user, const std::string& pin) const
{
  if (pin.size() != 4 || !std::all_of(pin.begin(), pin.end(), ::isdigit))
    return false;
  if (user.value("pin_hash", nlohmann::json()).is_null())
    return false;
  const std::string salt = user.value("pin_salt", "");
  const std::string hash = user.value("pin_hash", "");
  if (salt.empty() || hash.empty())
    return false;
  return CryptoUtil::hashPin(salt, pin) == hash;
}

bool AuthService::groupAllowsPinUnlocked(const std::string& group_id) const
{
  const auto* group = findGroupByIdUnlocked(group_id);
  return group && group->value("allow_pin", false);
}

bool AuthService::pinInUseUnlocked(const std::string& pin, const std::string& except_user_id) const
{
  if (pin.size() != 4)
    return false;
  for (const auto& user : data_["users"])
  {
    if (user.value("id", "") == except_user_id)
      continue;
    if (verifyPinUnlocked(user, pin))
      return true;
  }
  return false;
}

std::optional<AuthSession> AuthService::buildSessionUnlocked(const nlohmann::json& user)
{
  const auto* group = findGroupByIdUnlocked(user.value("group_id", ""));
  if (!group)
    return std::nullopt;

  AuthSession session;
  session.token = CryptoUtil::randomToken(32);
  session.user_id = user.value("id", "");
  session.username = user.value("username", "");
  session.group_id = group->value("id", "");
  session.group_name = group->value("name", "");
  session.permissions = group->value("permissions", nlohmann::json::object());
  return session;
}

nlohmann::json AuthService::userPublicView(const nlohmann::json& user, const nlohmann::json& group)
{
  return {{"id", user.value("id", "")},
          {"username", user.value("username", "")},
          {"display_name", user.value("display_name", "")},
          {"email", user.value("email", "")},
          {"group_id", user.value("group_id", "")},
          {"group_name", group.value("name", "")},
          {"permissions", group.value("permissions", nlohmann::json::object())},
          {"enabled", user.value("enabled", true)},
          {"has_pin", !user.value("pin_hash", nlohmann::json()).is_null()}};
}

std::optional<nlohmann::json> AuthService::loginPassword(const std::string& username,
                                                           const std::string& password,
                                                           std::string& err)
{
  std::lock_guard<std::mutex> lock(mu_);
  const auto* user = findUserByUsernameUnlocked(username);
  if (!user || !user->value("enabled", true))
  {
    err = "invalid credentials";
    return std::nullopt;
  }
  if (!verifyPasswordUnlocked(*user, password))
  {
    err = "invalid credentials";
    return std::nullopt;
  }
  auto session = buildSessionUnlocked(*user);
  if (!session)
  {
    err = "invalid user group";
    return std::nullopt;
  }
  sessions_[session->token] = *session;
  const auto* group = findGroupByIdUnlocked(user->value("group_id", ""));
  return nlohmann::json{{"token", session->token},
                        {"user", userPublicView(*user, group ? *group : nlohmann::json::object())}};
}

std::optional<nlohmann::json> AuthService::loginPin(const std::string& pin, std::string& err)
{
  std::lock_guard<std::mutex> lock(mu_);
  if (pin.size() != 4 || !std::all_of(pin.begin(), pin.end(), ::isdigit))
  {
    err = "pin must be 4 digits";
    return std::nullopt;
  }

  const nlohmann::json* matched = nullptr;
  for (const auto& user : data_["users"])
  {
    if (!user.value("enabled", true))
      continue;
    if (!groupAllowsPinUnlocked(user.value("group_id", "")))
      continue;
    if (verifyPinUnlocked(user, pin))
    {
      matched = &user;
      break;
    }
  }

  if (!matched)
  {
    err = "invalid pin";
    return std::nullopt;
  }

  auto session = buildSessionUnlocked(*matched);
  if (!session)
  {
    err = "invalid user group";
    return std::nullopt;
  }
  sessions_[session->token] = *session;
  const auto* group = findGroupByIdUnlocked(matched->value("group_id", ""));
  return nlohmann::json{{"token", session->token},
                        {"user", userPublicView(*matched, group ? *group : nlohmann::json::object())}};
}

bool AuthService::logout(const std::string& token)
{
  if (token.empty())
    return false;
  std::lock_guard<std::mutex> lock(mu_);
  return sessions_.erase(token) > 0;
}

std::optional<nlohmann::json> AuthService::sessionInfo(const std::string& token) const
{
  if (token.empty())
    return std::nullopt;
  std::lock_guard<std::mutex> lock(mu_);
  const auto it = sessions_.find(token);
  if (it == sessions_.end())
    return std::nullopt;
  const auto* user = findUserByIdUnlocked(it->second.user_id);
  if (!user)
    return std::nullopt;
  const auto* group = findGroupByIdUnlocked(user->value("group_id", ""));
  return userPublicView(*user, group ? *group : nlohmann::json::object());
}

bool AuthService::changePassword(const std::string& token,
                                 const std::string& current_password,
                                 const std::string& new_password,
                                 std::string& err)
{
  if (new_password.size() < 4)
  {
    err = "password too short";
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  const auto it = sessions_.find(token);
  if (it == sessions_.end())
  {
    err = "not authenticated";
    return false;
  }

  for (auto& user : data_["users"])
  {
    if (user.value("id", "") != it->second.user_id)
      continue;
    if (!verifyPasswordUnlocked(user, current_password))
    {
      err = "current password incorrect";
      return false;
    }
    const std::string salt = CryptoUtil::randomToken(16);
    user["password_salt"] = salt;
    user["password_hash"] = CryptoUtil::hashPassword(salt, new_password);
    saveUnlocked();
    return true;
  }

  err = "user not found";
  return false;
}

std::optional<nlohmann::json> AuthService::changeProfile(const std::string& token,
                                                         const nlohmann::json& payload,
                                                         std::string& err)
{
  if (!payload.is_object())
  {
    err = "invalid payload";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  const auto it = sessions_.find(token);
  if (it == sessions_.end())
  {
    err = "not authenticated";
    return std::nullopt;
  }

  for (auto& user : data_["users"])
  {
    if (user.value("id", "") != it->second.user_id)
      continue;
    if (payload.contains("display_name") && payload["display_name"].is_string())
    {
      const std::string name = StringUtil::trimCopy(payload["display_name"].get<std::string>());
      if (name.empty())
      {
        err = "display_name cannot be empty";
        return std::nullopt;
      }
      user["display_name"] = name;
    }
    saveUnlocked();
    const auto* group = findGroupByIdUnlocked(user.value("group_id", ""));
    return userPublicView(user, group ? *group : nlohmann::json::object());
  }

  err = "user not found";
  return std::nullopt;
}

bool AuthService::isBuiltinGroupId(const std::string& id)
{
  return id == "group_distributors" || id == "group_administrators" || id == "group_users";
}

nlohmann::json AuthService::normalizePermissions(const nlohmann::json& perms)
{
  static const char* kResources[] = {
      "dashboard", "config", "maps", "missions", "sounds", "integrations", "users"};
  nlohmann::json out = nlohmann::json::object();
  for (const char* key : kResources)
  {
    std::string level = "none";
    if (perms.is_object() && perms.contains(key) && perms[key].is_string())
      level = perms[key].get<std::string>();
    if (level != "none" && level != "read" && level != "write")
      level = "none";
    out[key] = level;
  }
  return out;
}

nlohmann::json AuthService::groupPublicView(const nlohmann::json& group, size_t user_count)
{
  return {{"id", group.value("id", "")},
          {"name", group.value("name", "")},
          {"allow_pin", group.value("allow_pin", false)},
          {"permissions", normalizePermissions(group.value("permissions", nlohmann::json::object()))},
          {"user_count", user_count},
          {"builtin", isBuiltinGroupId(group.value("id", ""))}};
}

bool AuthService::canManageGroupUnlocked(const std::string& editor_group_id,
                                         const std::string& target_group_id) const
{
  if (editor_group_id == "group_distributors")
    return true;
  return target_group_id != "group_distributors" && target_group_id != "group_administrators";
}

size_t AuthService::countUsersInGroupUnlocked(const std::string& group_id) const
{
  size_t count = 0;
  if (!data_.contains("users") || !data_["users"].is_array())
    return 0;
  for (const auto& u : data_["users"])
  {
    if (u.value("group_id", "") == group_id)
      ++count;
  }
  return count;
}

const nlohmann::json* AuthService::findGroupByNameUnlocked(const std::string& name,
                                                           const std::string& except_id) const
{
  const std::string needle = StringUtil::toLower(StringUtil::trimCopy(name));
  if (needle.empty() || !data_.contains("groups") || !data_["groups"].is_array())
    return nullptr;
  for (const auto& g : data_["groups"])
  {
    if (g.value("id", "") == except_id)
      continue;
    if (StringUtil::toLower(g.value("name", "")) == needle)
      return &g;
  }
  return nullptr;
}

nlohmann::json AuthService::listGroups() const
{
  std::lock_guard<std::mutex> lock(mu_);
  nlohmann::json out = nlohmann::json::array();
  for (const auto& g : data_["groups"])
  {
    const std::string id = g.value("id", "");
    out.push_back(groupPublicView(g, countUsersInGroupUnlocked(id)));
  }
  return out;
}

std::optional<nlohmann::json> AuthService::createGroup(const nlohmann::json& payload, std::string& err)
{
  const std::string name = StringUtil::trimCopy(payload.value("name", ""));
  if (name.empty())
  {
    err = "name required";
    return std::nullopt;
  }

  const AuthSession* session = activeSession();
  if (!session)
  {
    err = "authentication required";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (!canManageGroupUnlocked(session->group_id, ""))
  {
    err = "insufficient permissions to create user groups";
    return std::nullopt;
  }
  if (findGroupByNameUnlocked(name))
  {
    err = "group name already exists";
    return std::nullopt;
  }

  nlohmann::json group = {{"id", "group_" + IdUtil::newId()},
                          {"name", name},
                          {"allow_pin", payload.value("allow_pin", false)},
                          {"permissions", normalizePermissions(payload.value("permissions", nlohmann::json::object()))}};
  data_["groups"].push_back(group);
  saveUnlocked();
  return groupPublicView(group, 0);
}

std::optional<nlohmann::json> AuthService::updateGroup(const std::string& id,
                                                       const nlohmann::json& payload,
                                                       std::string& err)
{
  const AuthSession* session = activeSession();
  if (!session)
  {
    err = "authentication required";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (!canManageGroupUnlocked(session->group_id, id))
  {
    err = "insufficient permissions to edit this user group";
    return std::nullopt;
  }

  for (auto& group : data_["groups"])
  {
    if (group.value("id", "") != id)
      continue;

    if (payload.contains("name") && payload["name"].is_string())
    {
      const std::string name = StringUtil::trimCopy(payload["name"].get<std::string>());
      if (name.empty())
      {
        err = "name cannot be empty";
        return std::nullopt;
      }
      if (findGroupByNameUnlocked(name, id))
      {
        err = "group name already exists";
        return std::nullopt;
      }
      group["name"] = name;
    }
    if (payload.contains("allow_pin") && payload["allow_pin"].is_boolean())
      group["allow_pin"] = payload["allow_pin"].get<bool>();
    if (payload.contains("permissions"))
      group["permissions"] = normalizePermissions(payload["permissions"]);

    saveUnlocked();
    return groupPublicView(group, countUsersInGroupUnlocked(id));
  }

  err = "group not found";
  return std::nullopt;
}

bool AuthService::deleteGroup(const std::string& id, std::string& err)
{
  const AuthSession* session = activeSession();
  if (!session)
  {
    err = "authentication required";
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (!canManageGroupUnlocked(session->group_id, id))
  {
    err = "insufficient permissions to delete this user group";
    return false;
  }
  if (isBuiltinGroupId(id))
  {
    err = "cannot delete built-in user group";
    return false;
  }
  if (countUsersInGroupUnlocked(id) > 0)
  {
    err = "group still has users";
    return false;
  }

  auto& groups = data_["groups"];
  const auto it = std::remove_if(groups.begin(), groups.end(), [&](const nlohmann::json& g) {
    return g.value("id", "") == id;
  });
  if (it == groups.end())
  {
    err = "group not found";
    return false;
  }
  groups.erase(it, groups.end());
  saveUnlocked();
  return true;
}

nlohmann::json AuthService::listUsers() const
{
  std::lock_guard<std::mutex> lock(mu_);
  nlohmann::json out = nlohmann::json::array();
  for (const auto& u : data_["users"])
  {
    const auto* group = findGroupByIdUnlocked(u.value("group_id", ""));
    out.push_back(userPublicView(u, group ? *group : nlohmann::json::object()));
  }
  return out;
}

std::optional<nlohmann::json> AuthService::createUser(const nlohmann::json& payload, std::string& err)
{
  const std::string username = payload.value("username", "");
  const std::string password = payload.value("password", "");
  const std::string group_id = payload.value("group_id", "");
  if (username.empty() || password.empty() || group_id.empty())
  {
    err = "username, password and group_id required";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (findUserByUsernameUnlocked(username))
  {
    err = "username already exists";
    return std::nullopt;
  }
  if (!findGroupByIdUnlocked(group_id))
  {
    err = "unknown group";
    return std::nullopt;
  }

  const std::string id = "user_" + IdUtil::newId();
  auto user = makeUser(id, username, password, group_id, payload.value("display_name", username));
  if (payload.contains("email") && payload["email"].is_string())
    user["email"] = StringUtil::trimCopy(payload["email"].get<std::string>());
  if (payload.contains("pin") && !payload["pin"].is_null())
  {
    const std::string pin = payload.value("pin", "");
    if (pin.size() != 4 || !std::all_of(pin.begin(), pin.end(), ::isdigit))
    {
      err = "pin must be 4 digits";
      return std::nullopt;
    }
    if (!groupAllowsPinUnlocked(group_id))
    {
      err = "group does not allow pin";
      return std::nullopt;
    }
    if (pinInUseUnlocked(pin))
    {
      err = "pin already in use";
      return std::nullopt;
    }
    const std::string pin_salt = CryptoUtil::randomToken(16);
    user["pin_salt"] = pin_salt;
    user["pin_hash"] = CryptoUtil::hashPin(pin_salt, pin);
  }

  data_["users"].push_back(user);
  saveUnlocked();
  const auto* group = findGroupByIdUnlocked(group_id);
  return userPublicView(user, group ? *group : nlohmann::json::object());
}

std::optional<nlohmann::json> AuthService::updateUser(const std::string& id,
                                                      const nlohmann::json& payload,
                                                      std::string& err)
{
  std::lock_guard<std::mutex> lock(mu_);
  for (auto& user : data_["users"])
  {
    if (user.value("id", "") != id)
      continue;

    if (payload.contains("display_name"))
      user["display_name"] = payload["display_name"];
    if (payload.contains("email"))
    {
      if (payload["email"].is_null())
        user["email"] = "";
      else if (payload["email"].is_string())
        user["email"] = StringUtil::trimCopy(payload["email"].get<std::string>());
    }
    if (payload.contains("username") && payload["username"].is_string())
    {
      const std::string username = StringUtil::trimCopy(payload["username"].get<std::string>());
      if (username.empty())
      {
        err = "username cannot be empty";
        return std::nullopt;
      }
      const auto* existing = findUserByUsernameUnlocked(username);
      if (existing && existing->value("id", "") != id)
      {
        err = "username already exists";
        return std::nullopt;
      }
      user["username"] = username;
    }
    if (payload.contains("enabled"))
      user["enabled"] = payload["enabled"];
    if (payload.contains("group_id"))
    {
      const std::string group_id = payload.value("group_id", "");
      if (!findGroupByIdUnlocked(group_id))
      {
        err = "unknown group";
        return std::nullopt;
      }
      user["group_id"] = group_id;
    }
    if (payload.contains("password") && payload["password"].is_string())
    {
      const std::string password = payload["password"];
      const std::string salt = CryptoUtil::randomToken(16);
      user["password_salt"] = salt;
      user["password_hash"] = CryptoUtil::hashPassword(salt, password);
    }
    if (payload.contains("pin"))
    {
      if (payload["pin"].is_null())
      {
        user["pin_salt"] = nullptr;
        user["pin_hash"] = nullptr;
      }
      else
      {
        const std::string pin = payload.value("pin", "");
        if (pin.size() != 4 || !std::all_of(pin.begin(), pin.end(), ::isdigit))
        {
          err = "pin must be 4 digits";
          return std::nullopt;
        }
        if (!groupAllowsPinUnlocked(user.value("group_id", "")))
        {
          err = "group does not allow pin";
          return std::nullopt;
        }
        if (pinInUseUnlocked(pin, id))
        {
          err = "pin already in use";
          return std::nullopt;
        }
        const std::string pin_salt = CryptoUtil::randomToken(16);
        user["pin_salt"] = pin_salt;
        user["pin_hash"] = CryptoUtil::hashPin(pin_salt, pin);
      }
    }

    saveUnlocked();
    const auto* group = findGroupByIdUnlocked(user.value("group_id", ""));
    return userPublicView(user, group ? *group : nlohmann::json::object());
  }

  err = "user not found";
  return std::nullopt;
}

bool AuthService::deleteUser(const std::string& id, std::string& err)
{
  std::lock_guard<std::mutex> lock(mu_);
  auto& users = data_["users"];
  const auto it = std::remove_if(users.begin(), users.end(), [&](const nlohmann::json& u) {
    return u.value("id", "") == id;
  });
  if (it == users.end())
  {
    err = "user not found";
    return false;
  }
  users.erase(it, users.end());
  saveUnlocked();
  return true;
}

bool AuthService::authorizeApiRequest(const httplib::Request& req, httplib::Response& res)
{
  if (const char* disabled = std::getenv("LM_AUTH_DISABLED"); disabled && std::string(disabled) == "1")
    return true;

  const std::string token = extractToken(req);
  if (token.empty())
  {
    HttpUtil::jsonError(res, 401, "authentication required");
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  const auto it = sessions_.find(token);
  if (it == sessions_.end())
  {
    HttpUtil::jsonError(res, 401, "invalid or expired session");
    return false;
  }

  tls_session_ = &it->second;

  const auto resource = resourceForApiPath(req.path);
  if (!resource)
    return true;

  const bool write = requiresWrite(req.method);
  if (!write && req.path.rfind("/api/maps", 0) == 0 &&
      permissionAllows(it->second.permissions, "dashboard", false))
    return true;

  if (!permissionAllows(it->second.permissions, *resource, write))
  {
    HttpUtil::jsonError(res, 403, "insufficient permissions");
    return false;
  }
  return true;
}

httplib::Server::HandlerResponse AuthService::preRoute(const httplib::Request& req,
                                                       httplib::Response& res)
{
  tls_session_ = nullptr;

  if (req.path.rfind("/api/", 0) != 0)
    return httplib::Server::HandlerResponse::Unhandled;

  if (isPublicApiPath(req.path, req.method))
    return httplib::Server::HandlerResponse::Unhandled;

  if (!authorizeApiRequest(req, res))
  {
    HttpUtil::addCors(res);
    return httplib::Server::HandlerResponse::Handled;
  }

  return httplib::Server::HandlerResponse::Unhandled;
}

void AuthService::registerRoutes(httplib::Server& svr)
{
  svr.Post("/api/auth/login", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }

    std::string err;
    std::optional<nlohmann::json> result;
    if (body.contains("pin"))
      result = loginPin(body.value("pin", ""), err);
    else
      result = loginPassword(body.value("username", ""), body.value("password", ""), err);

    if (!result)
    {
      HttpUtil::jsonError(res, 401, err);
      return;
    }

    const std::string token = result->value("token", "");
    res.set_header("Set-Cookie",
                   std::string(kSessionCookie) + "=" + token + "; Path=/; HttpOnly; SameSite=Lax");
    res.set_content(result->dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Post("/api/auth/logout", [this](const httplib::Request& req, httplib::Response& res) {
    const std::string token = extractToken(req);
    logout(token);
    res.set_header("Set-Cookie", std::string(kSessionCookie) + "=; Path=/; HttpOnly; Max-Age=0");
    res.set_content(R"({"ok":true})", "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Get("/api/auth/me", [this](const httplib::Request& req, httplib::Response& res) {
    HttpUtil::addCors(res);
    res.set_header("Content-Type", "application/json; charset=utf-8");
    const std::string token = extractToken(req);
    const auto info = sessionInfo(token);
    if (!info)
    {
      res.set_content(R"({"user":null})", "application/json; charset=utf-8");
      return;
    }
    res.set_content(nlohmann::json({{"user", *info}}).dump(), "application/json; charset=utf-8");
  });

  svr.Put("/api/auth/password", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }

    const std::string token = extractToken(req);
    std::string err;
    if (!changePassword(token,
                        body.value("current_password", ""),
                        body.value("new_password", ""),
                        err))
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.set_content(R"({"ok":true})", "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Put("/api/auth/profile", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }

    const std::string token = extractToken(req);
    std::string err;
    const auto profile = changeProfile(token, body, err);
    if (!profile)
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    nlohmann::json out = {{"user", *profile}};
    res.set_content(out.dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Get("/api/user_groups", [this](const httplib::Request&, httplib::Response& res) {
    nlohmann::json out = {{"groups", listGroups()}};
    res.set_content(out.dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Post("/api/user_groups", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }
    std::string err;
    const auto group = createGroup(body, err);
    if (!group)
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.status = 201;
    res.set_content(group->dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Put(R"(/api/user_groups/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }
    std::string err;
    const auto group = updateGroup(req.matches[1].str(), body, err);
    if (!group)
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.set_content(group->dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Delete(R"(/api/user_groups/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
    std::string err;
    if (!deleteGroup(req.matches[1].str(), err))
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.set_content(R"({"ok":true})", "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Get("/api/users", [this](const httplib::Request&, httplib::Response& res) {
    nlohmann::json out = {{"users", listUsers()}};
    res.set_content(out.dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Post("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }
    std::string err;
    const auto user = createUser(body, err);
    if (!user)
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.status = 201;
    res.set_content(user->dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Put(R"(/api/users/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
    nlohmann::json body;
    try
    {
      body = nlohmann::json::parse(req.body);
    }
    catch (...)
    {
      HttpUtil::jsonError(res, 400, "invalid json");
      return;
    }
    std::string err;
    const auto user = updateUser(req.matches[1].str(), body, err);
    if (!user)
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.set_content(user->dump(), "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });

  svr.Delete(R"(/api/users/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
    const std::string id = req.matches[1].str();
    if (const AuthSession* session = activeSession())
    {
      if (session->user_id == id)
      {
        HttpUtil::jsonError(res, 400, "cannot delete current user");
        return;
      }
    }
    std::string err;
    if (!deleteUser(id, err))
    {
      HttpUtil::jsonError(res, 400, err);
      return;
    }
    res.set_content(R"({"ok":true})", "application/json; charset=utf-8");
    HttpUtil::addCors(res);
  });
}

}  // namespace lm
