#include <frostbite2D/resource/save_system.h>
#include <SDL2/SDL_log.h>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <frostbite2D/resource/asset.h>

namespace frostbite2D {

namespace {
constexpr int32 kDefaultSlotCount = 10;
constexpr int32 kMinSlotCount = 1;
constexpr int32 kCurrentSchemaVersion = 1;
constexpr int32 kCurrentIndexVersion = 1;
} // namespace

SaveSystem& SaveSystem::get() {
  static SaveSystem instance;
  return instance;
}

bool SaveSystem::init(const SaveSystemConfig& config) {
  config_ = config;

  if (config_.slotCount < kMinSlotCount) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "SaveSystem invalid slot count %d, fallback to %d",
                config_.slotCount, kDefaultSlotCount);
    config_.slotCount = kDefaultSlotCount;
  }

  if (config_.saveRoot.empty()) {
    config_.saveRoot = ".save";
  }
  if (config_.indexFile.empty()) {
    config_.indexFile = "index.json";
  }
  if (config_.slotPrefix.empty()) {
    config_.slotPrefix = "slot_";
  }

  resetSlots();
  initialized_ = true;

  if (!ensureSaveDirectory()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to create save root: %s",
                 config_.saveRoot.c_str());
    initialized_ = false;
    return false;
  }

  if (!refreshSlotsFromDisk()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to refresh slot metadata");
    initialized_ = false;
    return false;
  }

  if (!writeIndexFile()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to write index file");
    initialized_ = false;
    return false;
  }

  SDL_Log("SaveSystem initialized at %s with %d slots",
          config_.saveRoot.c_str(), config_.slotCount);
  return true;
}

bool SaveSystem::saveSlot(int32 slot, const nlohmann::json& payload,
                          const SaveMeta& meta) {
  if (!initialized_) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem not initialized before saveSlot");
    return false;
  }
  if (!isValidSlot(slot)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SaveSystem invalid slot: %d",
                 slot);
    return false;
  }

  Asset& asset = Asset::get();
  if (!ensureSaveDirectory()) {
    return false;
  }

  nlohmann::json document;
  document["schemaVersion"] = kCurrentSchemaVersion;
  document["savedAtUtc"] = makeUtcTimestamp();
  document["slot"] = slot;
  document["meta"] = toJson(meta);
  document["payload"] = payload;

  const std::string targetPath = slotPath(slot);
  const std::string tempPath = targetPath + ".tmp";

  if (!asset.writeTextFile(tempPath, document.dump(2), false)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to write temp slot file: %s",
                 tempPath.c_str());
    return false;
  }

  if (asset.exists(targetPath) && !asset.removeFile(targetPath)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to remove old slot file: %s",
                 targetPath.c_str());
    asset.removeFile(tempPath);
    return false;
  }

  bool moved = asset.moveFile(tempPath, targetPath);
  if (!moved) {
    if (!asset.copyFile(tempPath, targetPath, true)) {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                   "SaveSystem failed to move temp slot file: %s -> %s",
                   tempPath.c_str(), targetPath.c_str());
      asset.removeFile(tempPath);
      return false;
    }
    asset.removeFile(tempPath);
  }

  SaveSlotInfo& info = slots_[static_cast<size_t>(slot)];
  info.slot = slot;
  info.exists = true;
  info.updatedAtUtc = document.value("savedAtUtc", "");
  info.fileSize = asset.getFileSize(targetPath);
  info.schemaVersion = document.value("schemaVersion", kCurrentSchemaVersion);
  info.meta = meta;

  if (!writeIndexFile()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to update index after saving slot %d", slot);
    return false;
  }

  return true;
}

std::optional<nlohmann::json> SaveSystem::loadSlotPayload(int32 slot) const {
  if (!initialized_) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem not initialized before loadSlotPayload");
    return std::nullopt;
  }
  if (!isValidSlot(slot)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SaveSystem invalid slot: %d",
                 slot);
    return std::nullopt;
  }

  nlohmann::json document;
  if (!tryReadSlotDocument(slot, document)) {
    return std::nullopt;
  }

  if (!document.contains("payload")) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem slot file missing payload field: %d", slot);
    return std::nullopt;
  }

  return document["payload"];
}

std::optional<SaveSlotInfo> SaveSystem::getSlotInfo(int32 slot) const {
  if (!initialized_ || !isValidSlot(slot)) {
    return std::nullopt;
  }
  return slots_[static_cast<size_t>(slot)];
}

std::vector<SaveSlotInfo> SaveSystem::listSlots() const {
  return slots_;
}

bool SaveSystem::deleteSlot(int32 slot) {
  if (!initialized_) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem not initialized before deleteSlot");
    return false;
  }
  if (!isValidSlot(slot)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SaveSystem invalid slot: %d",
                 slot);
    return false;
  }

  Asset& asset = Asset::get();
  std::string path = slotPath(slot);
  if (asset.exists(path) && !asset.removeFile(path)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to delete slot file: %s", path.c_str());
    return false;
  }

  SaveSlotInfo cleared;
  cleared.slot = slot;
  slots_[static_cast<size_t>(slot)] = cleared;

  if (!writeIndexFile()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to update index after deleting slot %d",
                 slot);
    return false;
  }
  return true;
}

bool SaveSystem::hasSlot(int32 slot) const {
  if (!initialized_ || !isValidSlot(slot)) {
    return false;
  }
  return slots_[static_cast<size_t>(slot)].exists;
}

bool SaveSystem::isValidSlot(int32 slot) const {
  return slot >= 0 && slot < config_.slotCount;
}

void SaveSystem::resetSlots() {
  slots_.clear();
  slots_.resize(static_cast<size_t>(config_.slotCount));
  for (int32 slot = 0; slot < config_.slotCount; ++slot) {
    slots_[static_cast<size_t>(slot)].slot = slot;
  }
}

bool SaveSystem::ensureSaveDirectory() const {
  Asset& asset = Asset::get();
  if (asset.isDirectory(config_.saveRoot)) {
    return true;
  }

  if (asset.createDirectories(config_.saveRoot)) {
    return true;
  }

  return asset.isDirectory(config_.saveRoot);
}

bool SaveSystem::refreshSlotsFromDisk() {
  Asset& asset = Asset::get();
  for (int32 slot = 0; slot < config_.slotCount; ++slot) {
    SaveSlotInfo info;
    info.slot = slot;

    const std::string path = slotPath(slot);
    if (!asset.isRegularFile(path)) {
      slots_[static_cast<size_t>(slot)] = info;
      continue;
    }

    info.exists = true;
    info.fileSize = asset.getFileSize(path);

    nlohmann::json document;
    if (tryReadSlotDocument(slot, document)) {
      info.updatedAtUtc = document.value("savedAtUtc", "");
      info.schemaVersion =
          document.value("schemaVersion", kCurrentSchemaVersion);
      if (document.contains("meta") && document["meta"].is_object()) {
        info.meta = fromJson(document["meta"]);
      }
    } else {
      info.schemaVersion = 0;
    }

    slots_[static_cast<size_t>(slot)] = info;
  }

  return true;
}

bool SaveSystem::tryReadSlotDocument(int32 slot, nlohmann::json& outDoc) const {
  Asset& asset = Asset::get();

  std::string content;
  if (!asset.readTextFile(slotPath(slot), content)) {
    return false;
  }

  try {
    outDoc = nlohmann::json::parse(content);
    if (!outDoc.is_object()) {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                   "SaveSystem slot json root is not object: %d", slot);
      return false;
    }
  } catch (const nlohmann::json::exception& e) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "SaveSystem failed to parse slot json %d: %s", slot,
                 e.what());
    return false;
  }

  return true;
}

bool SaveSystem::writeIndexFile() const {
  if (!initialized_) {
    return false;
  }
  if (!ensureSaveDirectory()) {
    return false;
  }

  nlohmann::json root;
  root["schemaVersion"] = kCurrentIndexVersion;
  root["slotCount"] = config_.slotCount;
  root["slots"] = nlohmann::json::array();

  for (const SaveSlotInfo& info : slots_) {
    nlohmann::json slotJson;
    slotJson["slot"] = info.slot;
    slotJson["exists"] = info.exists;
    slotJson["updatedAtUtc"] = info.updatedAtUtc;
    slotJson["fileSize"] = info.fileSize;
    slotJson["schemaVersion"] = info.schemaVersion;
    slotJson["meta"] = toJson(info.meta);
    root["slots"].push_back(slotJson);
  }

  Asset& asset = Asset::get();
  return asset.writeTextFile(indexPath(), root.dump(2), false);
}

std::string SaveSystem::indexPath() const {
  return config_.saveRoot + "/" + config_.indexFile;
}

std::string SaveSystem::slotPath(int32 slot) const {
  return config_.saveRoot + "/" + slotFileName(slot);
}

std::string SaveSystem::slotFileName(int32 slot) const {
  char buffer[32] = {};
  std::snprintf(buffer, sizeof(buffer), "%s%02d.json", config_.slotPrefix.c_str(),
                static_cast<int>(slot));
  return buffer;
}

std::string SaveSystem::makeUtcTimestamp() {
  using namespace std::chrono;
  std::time_t nowTime = system_clock::to_time_t(system_clock::now());
  std::tm utc {};
#ifdef _WIN32
  gmtime_s(&utc, &nowTime);
#else
  gmtime_r(&nowTime, &utc);
#endif

  char buffer[32] = {};
  std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &utc);
  return buffer;
}

nlohmann::json SaveSystem::toJson(const SaveMeta& meta) {
  nlohmann::json metaJson;
  metaJson["displayName"] = meta.displayName;
  metaJson["playTimeSeconds"] = meta.playTimeSeconds;
  metaJson["userTag"] = meta.userTag;
  return metaJson;
}

SaveMeta SaveSystem::fromJson(const nlohmann::json& json) {
  SaveMeta meta;
  if (!json.is_object()) {
    return meta;
  }
  meta.displayName = json.value("displayName", "");
  meta.playTimeSeconds = json.value("playTimeSeconds", 0.0);
  meta.userTag = json.value("userTag", "");
  return meta;
}

} // namespace frostbite2D
