/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2024 Xenia Canary. All rights reserved.                          *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#include <regex>

#include "xenia/kernel/xam/profile_manager.h"

#include "xenia/base/logging.h"
#include "xenia/emulator.h"
#include "xenia/hid/input_system.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/crypto_utils.h"
#include "xenia/vfs/devices/host_path_device.h"

DEFINE_string(logged_profile_slot_0_xuid, "",
              "XUID of the profile to load on boot in slot 0", "Profiles");
DEFINE_string(logged_profile_slot_1_xuid, "",
              "XUID of the profile to load on boot in slot 1", "Profiles");
DEFINE_string(logged_profile_slot_2_xuid, "",
              "XUID of the profile to load on boot in slot 2", "Profiles");
DEFINE_string(logged_profile_slot_3_xuid, "",
              "XUID of the profile to load on boot in slot 3", "Profiles");

namespace xe {
namespace kernel {
namespace xam {

bool ProfileManager::DecryptAccountFile(const uint8_t* data,
                                        X_XAMACCOUNTINFO* output, bool devkit) {
  const uint8_t* key = util::GetXeKey(0x19, devkit);
  if (!key) {
    return false;  // this shouldn't happen...
  }

  // Generate RC4 key from data hash
  uint8_t rc4_key[0x14];
  util::HmacSha(key, 0x10, data, 0x10, 0, 0, 0, 0, rc4_key, 0x14);

  uint8_t dec_data[sizeof(X_XAMACCOUNTINFO) + 8];

  // Decrypt data
  util::RC4(rc4_key, 0x10, data + 0x10, sizeof(dec_data), dec_data,
            sizeof(dec_data));

  // Verify decrypted data against hash
  uint8_t data_hash[0x14];
  util::HmacSha(key, 0x10, dec_data, sizeof(dec_data), 0, 0, 0, 0, data_hash,
                0x14);

  if (std::memcmp(data, data_hash, 0x10) == 0) {
    // Copy account data to output
    std::memcpy(output, dec_data + 8, sizeof(X_XAMACCOUNTINFO));
    return true;
  }

  return false;
}

void ProfileManager::EncryptAccountFile(const X_XAMACCOUNTINFO* input,
                                        uint8_t* output, bool devkit) {
  const uint8_t* key = util::GetXeKey(0x19, devkit);
  if (!key) {
    return;  // this shouldn't happen...
  }

  X_XAMACCOUNTINFO* output_acct =
      reinterpret_cast<X_XAMACCOUNTINFO*>(output + 0x18);
  std::memcpy(output_acct, input, sizeof(X_XAMACCOUNTINFO));

  // Set confounder, should be random but meh
  std::memset(output + 0x10, 0xFD, 8);

  // Encrypted data = xam account info + 8 byte confounder
  uint32_t enc_data_size = sizeof(X_XAMACCOUNTINFO) + 8;

  // Set data hash
  uint8_t data_hash[0x14];
  util::HmacSha(key, 0x10, output + 0x10, enc_data_size, 0, 0, 0, 0, data_hash,
                0x14);

  std::memcpy(output, data_hash, 0x10);

  // Generate RC4 key from data hash
  uint8_t rc4_key[0x14];
  util::HmacSha(key, 0x10, data_hash, 0x10, 0, 0, 0, 0, rc4_key, 0x14);

  // Encrypt data
  util::RC4(rc4_key, 0x10, output + 0x10, enc_data_size, output + 0x10,
            enc_data_size);
}

ProfileManager::ProfileManager(KernelState* kernel_state,
                               UserTracker* user_tracker)
    : kernel_state_(kernel_state), user_tracker_(user_tracker) {
  logged_profiles_.clear();
  accounts_.clear();

  for (const auto account_xuid : FindProfiles()) {
    LoadAccount(account_xuid);
  }

  if (!cvars::logged_profile_slot_0_xuid.empty()) {
    Login(xe::string_util::from_string<uint64_t>(
              cvars::logged_profile_slot_0_xuid, true),
          0);
  }

  if (!cvars::logged_profile_slot_1_xuid.empty()) {
    Login(xe::string_util::from_string<uint64_t>(
              cvars::logged_profile_slot_1_xuid, true),
          1);
  }

  if (!cvars::logged_profile_slot_2_xuid.empty()) {
    Login(xe::string_util::from_string<uint64_t>(
              cvars::logged_profile_slot_2_xuid, true),
          2);
  }

  if (!cvars::logged_profile_slot_3_xuid.empty()) {
    Login(xe::string_util::from_string<uint64_t>(
              cvars::logged_profile_slot_3_xuid, true),
          3);
  }
}

void ProfileManager::ReloadProfile(const uint64_t xuid) {
  if (accounts_.contains(xuid)) {
    accounts_.erase(xuid);
  }

  LoadAccount(xuid);
}

void ProfileManager::ReloadProfiles() {
  for (const auto account_xuid : FindProfiles()) {
    LoadAccount(account_xuid);
  }
}

UserProfile* ProfileManager::GetProfile(const uint64_t xuid) const {
  const uint8_t user_index = GetUserIndexAssignedToProfile(xuid);
  if (user_index >= XUserMaxUserCount) {
    return nullptr;
  }

  return GetProfile(user_index);
}

UserProfile* ProfileManager::GetProfile(uint8_t user_index) const {
  if (user_index == XUserIndexLatest) {
    user_index = kernel_state_->emulator()->input_system()->GetLastUsedSlot();
  }

  if (user_index == XUserIndexNone) {
    return nullptr;
  }

  if (user_index == XUserIndexAny) {
    for (uint8_t i = 0; i < XUserMaxUserCount; i++) {
      if (!logged_profiles_.count(i)) {
        continue;
      }

      return logged_profiles_.at(i).get();
    }
  }

  if (!logged_profiles_.count(user_index)) {
    return nullptr;
  }

  return logged_profiles_.at(user_index).get();
}

bool ProfileManager::LoadAccount(const uint64_t xuid) {
  const std::string xuid_as_string = fmt::format("{:016X}", xuid);

  XELOGI("{}: Loading Account: {}", __func__, xuid_as_string);

  MountProfile(xuid);

  const std::string guest_path =
      fmt::format(kDefaultMountFormat, xuid) + ":\\Account";

  xe::vfs::File* output_file;
  xe::vfs::FileAction action = {};
  auto status = kernel_state_->file_system()->OpenFile(
      nullptr, guest_path, xe::vfs::FileDisposition::kOpen,
      xe::vfs::FileAccess::kFileReadData, false, true, &output_file, &action);

  if (XFAILED(status) || !output_file || !output_file->entry()) {
    XELOGI("{}: Failed to open Account file: {:08X}", __func__, status);
    DismountProfile(xuid);
    return false;
  }

  std::vector<uint8_t> file_data;
  file_data.resize(output_file->entry()->size());

  size_t bytes_read = 0;
  output_file->ReadSync(
      std::span<uint8_t>(file_data.data(), output_file->entry()->size()), 0,
      &bytes_read);
  output_file->Destroy();

  if (bytes_read < sizeof(X_XAMACCOUNTINFO)) {
    DismountProfile(xuid);
    return false;
  }

  X_XAMACCOUNTINFO tmp_acct;
  if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct)) {
    if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct,
                                            true)) {
      XELOGW("Failed to decrypt account data file for XUID: {}",
             xuid_as_string);
      return false;
    }
  }

  // We don't need profile to be mounted anymore, so for now let's close it.
  // We need it only when we want to login into this account!
  DismountProfile(xuid);

  accounts_.insert_or_assign(xuid, tmp_acct);
  return true;
}

bool ProfileManager::MountProfile(const uint64_t xuid, std::string mount_path) {
  std::filesystem::path profile_path = GetProfilePath(xuid);
  if (mount_path.empty()) {
    mount_path = fmt::format(kDefaultMountFormat, xuid);
  }
  mount_path += ':';

  auto device =
      std::make_unique<vfs::HostPathDevice>(mount_path, profile_path, false);
  if (!device->Initialize()) {
    XELOGE(
        "MountProfile: Unable to mount {} profile; file not found or "
        "corrupted.",
        profile_path);
    return false;
  }
  return kernel_state_->file_system()->RegisterDevice(std::move(device));
}

bool ProfileManager::DismountProfile(const uint64_t xuid) {
  return kernel_state_->file_system()->UnregisterDevice(
      fmt::format(kDefaultMountFormat, xuid) + ':');
}

void ProfileManager::Login(const uint64_t xuid, const uint8_t user_index,
                           bool notify) {
  if (logged_profiles_.size() >= XUserMaxUserCount) {
    XELOGE(
        "Cannot login account with XUID: {:016X} due to lack of free slots "
        "(Max 4 accounts at once)",
        xuid);
    return;
  }

  if (user_index < XUserMaxUserCount) {
    const auto& profile = logged_profiles_.find(user_index);
    if (profile != logged_profiles_.cend()) {
      if (profile->second && profile->second->xuid() == xuid) {
        // Do nothing! User is already signed in to that slot.
        return;
      }
    }
  }

  // Find if xuid is already logged in. We might want to logout.
  auto it = std::find_if(
      logged_profiles_.begin(), logged_profiles_.end(),
      [xuid](const auto& entry) { return entry.second->xuid() == xuid; });
  if (it != logged_profiles_.end()) {
    Logout(it->first);
  }

  if (!accounts_.count(xuid)) {
    return;
  }

  auto& profile = accounts_[xuid];
  const uint8_t assigned_user_slot =
      user_index < XUserMaxUserCount ? user_index : FindFirstFreeProfileSlot();

  XELOGI("Loaded {} (GUID: {:016X}) to slot {}", profile.GetGamertagString(),
         xuid, assigned_user_slot);

  MountProfile(xuid);

  logged_profiles_[assigned_user_slot] =
      std::make_unique<UserProfile>(xuid, &profile);

  user_tracker_->AddUser(xuid);

  if (notify) {
    kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged,
                                         GetUsedUserSlots().to_ulong());
  }
  UpdateConfig(xuid, assigned_user_slot);
}

void ProfileManager::Logout(const uint8_t user_index, bool notify) {
  auto profile = logged_profiles_.find(user_index);
  if (profile == logged_profiles_.cend()) {
    return;
  }

  kernel_state_->xam_state()->user_tracker()->RemoveUser(
      profile->second->xuid());
  DismountProfile(profile->second->xuid());
  logged_profiles_.erase(profile);
  if (notify) {
    kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged,
                                         GetUsedUserSlots().to_ulong());
  }
  UpdateConfig(0, user_index);
}

void ProfileManager::LoginMultiple(
    const std::map<uint8_t, uint64_t>& profiles) {
  int slots_mask = 0;
  for (auto [slot, xuid] : profiles) {
    Login(xuid, slot, false);
    slots_mask |= (1 << slot);
  }

  kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged,
                                       slots_mask);
}

std::vector<uint64_t> ProfileManager::FindProfiles() const {
  // Info: Profile directory name is also it's offline xuid
  std::vector<uint64_t> profiles_xuids;

  auto profiles_directory = xe::filesystem::FilterByName(
      xe::filesystem::ListDirectories(
          kernel_state_->emulator()->content_root()),
      std::regex("[0-9A-F]{16}"));

  for (const auto& profile : profiles_directory) {
    const std::string profile_xuid = xe::path_to_utf8(profile.name);
    if (profile_xuid == fmt::format("{:016X}", 0)) {
      continue;
    }

    if (!std::filesystem::exists(
            profile.path / profile.name / kDashboardStringID /
            fmt::format("{:08X}",
                        static_cast<uint32_t>(XContentType::kProfile)) /
            profile.name)) {
      XELOGE("Profile {} doesn't have profile package!", profile_xuid);
      continue;
    }

    XELOGI("{}: Adding profile {} to profile list", __func__, profile_xuid);
    profiles_xuids.push_back(
        xe::string_util::from_string<uint64_t>(profile_xuid, true));
  }

  XELOGI("ProfileManager: Found {} Profiles", profiles_xuids.size());
  return profiles_xuids;
}

uint8_t ProfileManager::FindFirstFreeProfileSlot() const {
  if (!IsAnyProfileSignedIn()) {
    return 0;
  }

  std::bitset<XUserMaxUserCount> used_slots = {};
  for (const auto& [index, entry] : logged_profiles_) {
    used_slots.set(index);
  }

  for (uint8_t i = 0; i < used_slots.size(); ++i) {
    if (!used_slots[i]) {
      return i;
    }
  }
  return XUserIndexAny;
}

std::bitset<XUserMaxUserCount> ProfileManager::GetUsedUserSlots() const {
  std::bitset<XUserMaxUserCount> used_slots = {};
  for (const auto& [index, entry] : logged_profiles_) {
    if (!entry) {
      continue;
    }

    used_slots.set(index);
  }

  return used_slots;
}

uint8_t ProfileManager::GetUserIndexAssignedToProfile(
    const uint64_t xuid) const {
  for (const auto& [index, entry] : logged_profiles_) {
    if (!entry) {
      continue;
    }

    if (entry->xuid() != xuid) {
      continue;
    }

    return index;
  }
  return XUserIndexAny;
}

std::filesystem::path ProfileManager::GetProfileContentPath(
    const uint64_t xuid, const uint32_t title_id,
    const XContentType content_type) const {
  std::filesystem::path profile_content_path =
      kernel_state_->emulator()->content_root() / fmt::format("{:016X}", xuid);
  if (title_id != -1 && title_id != 0) {
    profile_content_path =
        profile_content_path / fmt::format("{:08X}", title_id);

    if (content_type != XContentType::kInvalid) {
      profile_content_path =
          profile_content_path /
          fmt::format("{:08X}", static_cast<uint32_t>(content_type));
    }
  }
  return profile_content_path;
}

std::filesystem::path ProfileManager::GetProfilePath(
    const uint64_t xuid) const {
  return GetProfilePath(fmt::format("{:016X}", xuid));
}

std::filesystem::path ProfileManager::GetProfilePath(
    const std::string xuid) const {
  return kernel_state_->emulator()->content_root() / xuid / kDashboardStringID /
         fmt::format("{:08X}", static_cast<uint32_t>(XContentType::kProfile)) /
         xuid;
}

bool ProfileManager::CreateProfile(const std::string gamertag, bool autologin,
                                   bool default_xuid) {
  const auto xuid = !default_xuid ? GenerateXuid() : 0xB13EBABEBABEBABE;

  if (!std::filesystem::create_directories(GetProfilePath(xuid))) {
    return false;
  }

  if (!MountProfile(xuid)) {
    return false;
  }

  const bool is_account_created = CreateAccount(xuid, gamertag);
  if (is_account_created && autologin) {
    Login(xuid);
  }
  return is_account_created;
}

bool ProfileManager::CreateProfile(const X_XAMACCOUNTINFO* account_info,
                                   uint64_t xuid) {
  if (!xuid) {
    xuid = GenerateXuid();
  }

  if (!std::filesystem::create_directories(GetProfilePath(xuid))) {
    return false;
  }

  if (!MountProfile(xuid)) {
    return false;
  }

  return CreateAccount(xuid, account_info);
}

const X_XAMACCOUNTINFO* ProfileManager::GetAccount(const uint64_t xuid) {
  if (!accounts_.count(xuid)) {
    return nullptr;
  }

  return &accounts_[xuid];
}

bool ProfileManager::CreateAccount(const uint64_t xuid,
                                   const std::string gamertag) {
  X_XAMACCOUNTINFO account = {};
  const std::u16string gamertag_u16 = xe::to_utf16(gamertag);

  string_util::copy_and_swap_truncating(account.gamertag, gamertag_u16,
                                        sizeof(account.gamertag));

  const bool result = UpdateAccount(xuid, &account);
  DismountProfile(xuid);

  if (result) {
    accounts_.insert({xuid, account});
  }
  return result;
}

bool ProfileManager::CreateAccount(const uint64_t xuid,
                                   const X_XAMACCOUNTINFO* account) {
  const bool result = UpdateAccount(xuid, account);
  if (result) {
    accounts_.insert({xuid, *account});
  }
  return result;
}

bool ProfileManager::UpdateAccount(const uint64_t xuid,
                                   const X_XAMACCOUNTINFO* account) {
  const std::string guest_path =
      fmt::format(kDefaultMountFormat, xuid) + ":\\Account";

  xe::vfs::File* output_file;
  xe::vfs::FileAction action = {};
  auto status = kernel_state_->file_system()->OpenFile(
      nullptr, guest_path, xe::vfs::FileDisposition::kOpenIf,
      xe::vfs::FileAccess::kFileWriteData, false, true, &output_file, &action);

  if (XFAILED(status) || !output_file || !output_file->entry()) {
    XELOGI("{}: Failed to open Account file for creation: {:08X}", __func__,
           status);
    return false;
  }

  std::vector<uint8_t> encrypted_data;
  encrypted_data.resize(sizeof(X_XAMACCOUNTINFO) + 0x18);
  EncryptAccountFile(account, encrypted_data.data());

  size_t written_bytes = 0;
  output_file->WriteSync(
      std::span<uint8_t>(encrypted_data.data(), encrypted_data.size()), 0,
      &written_bytes);
  output_file->Destroy();

  // Refresh the in-memory account data
  accounts_.insert_or_assign(xuid, *account);
  return true;
}

void ProfileManager::UpdateConfig(const uint64_t xuid, const uint8_t slot) {
  if (slot >= XUserMaxUserCount) {
    return;
  }

  const std::string hex_xuid = xe::string_util::to_hex_string(xuid);
  switch (slot) {
    case 0:
      OVERRIDE_string(logged_profile_slot_0_xuid, hex_xuid);
      break;
    case 1:
      OVERRIDE_string(logged_profile_slot_1_xuid, hex_xuid);
      break;
    case 2:
      OVERRIDE_string(logged_profile_slot_2_xuid, hex_xuid);
      break;
    case 3:
      OVERRIDE_string(logged_profile_slot_3_xuid, hex_xuid);
      break;
    default:
      break;
  }
  return;
}

bool ProfileManager::DeleteProfile(const uint64_t xuid) {
  const uint8_t user_index = GetUserIndexAssignedToProfile(xuid);

  if (user_index < XUserMaxUserCount) {
    Logout(user_index);
  }

  DismountProfile(xuid);

  if (accounts_.count(xuid)) {
    accounts_.erase(xuid);
  }

  std::error_code ec;
  std::filesystem::remove_all(GetProfileContentPath(xuid), ec);
  if (ec) {
    XELOGE("Cannot remove profile: {}", ec.message());
    return false;
  }
  return true;
}

bool ProfileManager::IsGamertagValid(const std::string gamertag) {
  std::regex pattern(R"(^[A-Za-z][A-Za-z0-9]*( [A-Za-z0-9]+)*$)");

  if (gamertag.length() < 1 || gamertag.length() > 15) {
    return false;
  }

  return std::regex_match(gamertag, pattern);
}

}  // namespace xam
}  // namespace kernel
}  // namespace xe
