#include "storage/sound_store.hpp"

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

#include "util/string_util.hpp"

#include <sqlite3.h>
#include <algorithm>
#include <cctype>
#include <vector>

namespace lm {

namespace {

constexpr const char* kSoundSelect =
    "SELECT id, name, description, file_name, duration_ms, enabled, volume, is_system, created_at, updated_at "
    "FROM sounds";

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)));
  };
  return {{"id", textOrNull(0)},
          {"name", textOrNull(1)},
          {"description", textOrNull(2)},
          {"file_name", textOrNull(3)},
          {"duration_ms", sqlite3_column_type(stmt, 4) == SQLITE_NULL
                              ? nlohmann::json(nullptr)
                              : nlohmann::json(sqlite3_column_int(stmt, 4))},
          {"enabled", sqlite3_column_int(stmt, 5) != 0},
          {"volume", sqlite3_column_int(stmt, 6)},
          {"is_system", sqlite3_column_int(stmt, 7) != 0},
          {"created_at", textOrNull(8)},
          {"updated_at", textOrNull(9)}};
}

int clampVolume(int v)
{
  if (v < 0)
    return 0;
  if (v > 100)
    return 100;
  return v;
}

bool isReservedSystemName(const std::string& name)
{
  const std::string lower = StringUtil::toLower(StringUtil::trimCopy(name));
  return lower == "beep" || lower == "horn" || lower == "chime";
}

bool findNameConflict(sqlite3* db, const std::string& name, const std::string& except_id)
{
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db,
                         except_id.empty()
                             ? "SELECT id FROM sounds WHERE lower(name) = lower(?1) LIMIT 1"
                             : "SELECT id FROM sounds WHERE lower(name) = lower(?1) AND id != ?2 LIMIT 1",
                         -1,
                         &stmt,
                         nullptr) != SQLITE_OK)
    return false;
  sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
  if (!except_id.empty())
    sqlite3_bind_text(stmt, 2, except_id.c_str(), -1, SQLITE_TRANSIENT);
  const bool conflict = sqlite3_step(stmt) == SQLITE_ROW;
  sqlite3_finalize(stmt);
  return conflict;
}

}  // namespace

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

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

std::optional<nlohmann::json> SoundStore::find(const std::string& id) const
{
  std::lock_guard<std::mutex> lock(mu_);
  const std::string query = std::string(kSoundSelect) + " WHERE id = ?1";
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), query.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> SoundStore::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;
  }
  if (isReservedSystemName(name))
  {
    err = "name reserved for built-in system sounds";
    return std::nullopt;
  }

  std::lock_guard<std::mutex> lock(mu_);
  if (findNameConflict(db_.handle(), name, ""))
  {
    err = "sound name already exists";
    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 bool enabled = payload.value("enabled", true);
  const int volume = clampVolume(payload.value("volume", 100));
  const bool is_system = payload.value("is_system", false);

  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "INSERT INTO sounds(id, name, description, file_name, duration_ms, enabled, volume, "
                         "is_system, created_at, updated_at) "
                         "VALUES(?1,?2,?3,NULL,NULL,?4,?5,?6,?7,?8)",
                         -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);
  sqlite3_bind_int(stmt, 4, enabled ? 1 : 0);
  sqlite3_bind_int(stmt, 5, volume);
  sqlite3_bind_int(stmt, 6, is_system ? 1 : 0);
  sqlite3_bind_text(stmt, 7, now.c_str(), -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(stmt, 8, 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);

  return nlohmann::json{{"id", id},
                        {"name", name},
                        {"description", description},
                        {"file_name", nullptr},
                        {"duration_ms", nullptr},
                        {"enabled", enabled},
                        {"volume", volume},
                        {"is_system", is_system},
                        {"created_at", now},
                        {"updated_at", now}};
}

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

  const bool is_system = existing->value("is_system", false);
  nlohmann::json merged = *existing;
  for (const char* key : {"description", "enabled", "duration_ms", "volume"})
  {
    if (payload.contains(key))
      merged[key] = payload[key];
  }
  if (!is_system && payload.contains("name"))
  {
    const std::string new_name = StringUtil::trimCopy(payload["name"].get<std::string>());
    if (new_name.empty())
    {
      err = "name is required";
      return false;
    }
    if (isReservedSystemName(new_name))
    {
      err = "name reserved for built-in system sounds";
      return false;
    }
    if (findNameConflict(db_.handle(), new_name, id))
    {
      err = "sound name already exists";
      return false;
    }
    merged["name"] = new_name;
  }
  if (merged.contains("volume"))
    merged["volume"] = clampVolume(merged["volume"].get<int>());

  const std::string now = IdUtil::nowIso8601();
  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(),
                         "UPDATE sounds SET name=?2, description=?3, enabled=?4, duration_ms=?5, volume=?6, "
                         "updated_at=?7 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);
  sqlite3_bind_int(stmt, 4, merged.value("enabled", true) ? 1 : 0);
  if (merged["duration_ms"].is_number_integer())
    sqlite3_bind_int(stmt, 5, merged["duration_ms"].get<int>());
  else
    sqlite3_bind_null(stmt, 5);
  sqlite3_bind_int(stmt, 6, clampVolume(merged.value("volume", 100)));
  sqlite3_bind_text(stmt, 7, 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 SoundStore::remove(const std::string& id, std::string& err)
{
  auto existing = find(id);
  if (!existing)
  {
    err = "sound not found";
    return false;
  }
  if (existing->value("is_system", false))
  {
    err = "system sounds cannot be deleted";
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  sqlite3_stmt* stmt = nullptr;
  if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM sounds 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 (existing->contains("file_name") && (*existing)["file_name"].is_string())
  {
    const auto path = db_.soundsDir() / existing->value("file_name", "");
    std::error_code ec;
    std::filesystem::remove(path, ec);
  }
  return ok;
}

std::optional<std::filesystem::path> SoundStore::filePath(const std::string& id) const
{
  const auto sound = find(id);
  if (!sound || !(*sound)["file_name"].is_string())
    return std::nullopt;
  const auto path = db_.soundsDir() / sound->value("file_name", "");
  if (!std::filesystem::exists(path))
    return std::nullopt;
  return path;
}

bool SoundStore::saveFile(const std::string& id,
                          const std::string& filename,
                          const std::string& bytes,
                          std::string& err)
{
  auto existing = find(id);
  if (!existing)
  {
    err = "sound not found";
    return false;
  }
  if (existing->value("is_system", false))
  {
    err = "system sounds cannot be replaced";
    return false;
  }

  std::error_code ec;
  std::filesystem::create_directories(db_.soundsDir(), ec);
  const auto path = db_.soundsDir() / filename;
  if (!FileUtil::writeBinaryAtomic(path, bytes))
  {
    err = "failed to write sound 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 sounds SET file_name = ?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 SoundStore::ensureSystemDefaults(std::string& err)
{
  {
    std::vector<std::string> dup_ids;
    std::lock_guard<std::mutex> lock(mu_);
    sqlite3_stmt* stmt = nullptr;
    if (sqlite3_prepare_v2(db_.handle(),
                           "SELECT id FROM sounds WHERE is_system = 0 AND lower(name) IN ('beep', 'horn', 'chime')",
                           -1,
                           &stmt,
                           nullptr) == SQLITE_OK)
    {
      while (sqlite3_step(stmt) == SQLITE_ROW)
      {
        if (sqlite3_column_type(stmt, 0) != SQLITE_NULL)
          dup_ids.push_back(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)));
      }
      sqlite3_finalize(stmt);
    }
    for (const auto& dup_id : dup_ids)
    {
      sqlite3_stmt* del = nullptr;
      if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM sounds WHERE id = ?1", -1, &del, nullptr) == SQLITE_OK)
      {
        sqlite3_bind_text(del, 1, dup_id.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_step(del);
        sqlite3_finalize(del);
      }
    }
  }

  struct DefaultSound
  {
    const char* id;
    const char* name;
  };
  static const DefaultSound kDefaults[] = {
      {"sys_beep", "Beep"},
      {"sys_horn", "Horn"},
      {"sys_chime", "Chime"},
  };

  for (const auto& d : kDefaults)
  {
    if (find(d.id))
      continue;
    nlohmann::json payload = {{"id", d.id}, {"name", d.name}, {"is_system", true}, {"volume", 100}};
    if (!create(payload, err))
      return false;
  }
  return true;
}

}  // namespace lm
