#include "ap_state.h"

#define HAS_STD_FILESYSTEM
#define _WEBSOCKETPP_CPP11_STRICT_
#pragma comment(lib, "crypt32")

#include <fmt/core.h>
#include <hkutil/string.h>

#include <any>
#include <apclient.hpp>
#include <apuuid.hpp>
#include <chrono>
#include <exception>
#include <filesystem>
#include <list>
#include <memory>
#include <mutex>
#include <set>
#include <sstream>
#include <thread>
#include <tuple>

#include "game_data.h"
#include "logger.h"
#include "tracker_frame.h"
#include "tracker_state.h"

constexpr int AP_MAJOR = 0;
constexpr int AP_MINOR = 4;
constexpr int AP_REVISION = 5;

constexpr const char* CERT_STORE_PATH = "cacert.pem";
constexpr int ITEM_HANDLING = 7;  // <- all

namespace {

struct APState {
  std::unique_ptr<APClient> apclient;

  bool initialized = false;

  TrackerFrame* tracker_frame = nullptr;

  bool client_active = false;
  std::mutex client_mutex;

  bool connected = false;
  bool has_connection_result = false;

  std::string data_storage_prefix;
  std::list<std::string> tracked_data_storage_keys;
  std::string victory_data_storage_key;

  std::map<int64_t, int> inventory;
  std::set<int64_t> checked_locations;
  std::map<std::string, std::any> data_storage;
  std::optional<std::tuple<int, int>> player_pos;

  DoorShuffleMode door_shuffle_mode = kNO_DOORS;
  bool group_doors = false;
  bool color_shuffle = false;
  bool painting_shuffle = false;
  int mastery_requirement = 21;
  int level_2_requirement = 223;
  LocationChecks location_checks = kNORMAL_LOCATIONS;
  VictoryCondition victory_condition = kTHE_END;
  bool early_color_hallways = false;
  bool pilgrimage_enabled = false;
  bool pilgrimage_allows_roof_access = false;
  bool pilgrimage_allows_paintings = false;
  SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
  bool sunwarp_shuffle = false;

  std::map<std::string, std::string> painting_mapping;
  std::set<std::string> painting_codomain;
  std::map<int, SunwarpMapping> sunwarp_mapping;

  void Connect(std::string server, std::string player, std::string password) {
    if (!initialized) {
      TrackerLog("Initializing APState...");

      std::thread([this]() {
        for (;;) {
          {
            std::lock_guard client_guard(client_mutex);
            if (apclient) {
              apclient->poll();
            }
          }

          std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
      }).detach();

      for (int panel_id : GD_GetAchievementPanels()) {
        tracked_data_storage_keys.push_back(fmt::format(
            "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
      }

      for (const MapArea& map_area : GD_GetMapAreas()) {
        for (const Location& location : map_area.locations) {
          tracked_data_storage_keys.push_back(
              fmt::format("Hunt|{}", location.ap_location_id));
        }
      }

      tracked_data_storage_keys.push_back("PlayerPos");
      tracked_data_storage_keys.push_back("Paintings");

      initialized = true;
    }

    tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
    TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));

    {
      TrackerLog("Destroying old AP client...");

      std::lock_guard client_guard(client_mutex);

      if (apclient) {
        DestroyClient();
      }

      std::string cert_store = "";
      if (std::filesystem::exists(CERT_STORE_PATH)) {
        cert_store = CERT_STORE_PATH;
      }

      apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server,
                                            cert_store);
    }

    inventory.clear();
    checked_locations.clear();
    data_storage.clear();
    player_pos = std::nullopt;
    victory_data_storage_key.clear();
    door_shuffle_mode = kNO_DOORS;
    group_doors = false;
    color_shuffle = false;
    painting_shuffle = false;
    painting_mapping.clear();
    painting_codomain.clear();
    mastery_requirement = 21;
    level_2_requirement = 223;
    location_checks = kNORMAL_LOCATIONS;
    victory_condition = kTHE_END;
    early_color_hallways = false;
    pilgrimage_enabled = false;
    pilgrimage_allows_roof_access = false;
    pilgrimage_allows_paintings = false;
    sunwarp_access = kSUNWARP_ACCESS_NORMAL;
    sunwarp_shuffle = false;
    sunwarp_mapping.clear();

    std::mutex connection_mutex;
    connected = false;
    has_connection_result = false;

    apclient->set_room_info_handler([this, player, password]() {
      inventory.clear();

      TrackerLog(fmt::format(
          "Connected to Archipelago server. Authenticating as {} {}", player,
          (password.empty() ? "without password"
                            : "with password " + password)));
      tracker_frame->SetStatusMessage(
          "Connected to Archipelago server. Authenticating...");

      apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
                            {AP_MAJOR, AP_MINOR, AP_REVISION});
    });

    apclient->set_location_checked_handler(
        [this](const std::list<int64_t>& locations) {
          for (const int64_t location_id : locations) {
            checked_locations.insert(location_id);
            TrackerLog(fmt::format("Location: {}", location_id));
          }

          RefreshTracker(false);
        });

    apclient->set_slot_disconnected_handler([this]() {
      tracker_frame->SetStatusMessage(
          "Disconnected from Archipelago. Attempting to reconnect...");
      TrackerLog(
          "Slot disconnected from Archipelago. Attempting to reconnect...");
    });

    apclient->set_socket_disconnected_handler([this]() {
      tracker_frame->SetStatusMessage(
          "Disconnected from Archipelago. Attempting to reconnect...");
      TrackerLog(
          "Socket disconnected from Archipelago. Attempting to reconnect...");
    });

    apclient->set_items_received_handler(
        [this](const std::list<APClient::NetworkItem>& items) {
          for (const APClient::NetworkItem& item : items) {
            inventory[item.item]++;
            TrackerLog(fmt::format("Item: {}", item.item));
          }

          RefreshTracker(false);
        });

    apclient->set_retrieved_handler(
        [this](const std::map<std::string, nlohmann::json>& data) {
          for (const auto& [key, value] : data) {
            HandleDataStorage(key, value);
          }

          RefreshTracker(false);
        });

    apclient->set_set_reply_handler([this](const std::string& key,
                                           const nlohmann::json& value,
                                           const nlohmann::json&) {
      HandleDataStorage(key, value);
      RefreshTracker(false);
    });

    apclient->set_slot_connected_handler([this, player, server,
                                          &connection_mutex](
                                             const nlohmann::json& slot_data) {
      tracker_frame->SetStatusMessage(
          fmt::format("Connected to Archipelago! ({}@{})", player, server));
      TrackerLog("Connected to Archipelago!");

      data_storage_prefix =
          fmt::format("Lingo_{}_", apclient->get_player_number());
      door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
      if (slot_data.contains("group_doors")) {
        group_doors = slot_data.contains("group_doors") &&
                      slot_data["group_doors"].get<int>() == 1;
      } else {
        // If group_doors doesn't exist yet, that means kPANELS_MODE is actually
        // kSIMPLE_DOORS.
        if (door_shuffle_mode == kPANELS_MODE) {
          door_shuffle_mode = kDOORS_MODE;
          group_doors = true;
        }
      }
      color_shuffle = slot_data["shuffle_colors"].get<int>() == 1;
      painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
      mastery_requirement = slot_data["mastery_achievements"].get<int>();
      level_2_requirement = slot_data["level_2_requirement"].get<int>();
      location_checks = slot_data["location_checks"].get<LocationChecks>();
      victory_condition =
          slot_data["victory_condition"].get<VictoryCondition>();
      early_color_hallways = slot_data.contains("early_color_hallways") &&
                             slot_data["early_color_hallways"].get<int>() == 1;
      pilgrimage_enabled = slot_data.contains("enable_pilgrimage") &&
                           slot_data["enable_pilgrimage"].get<int>() == 1;
      pilgrimage_allows_roof_access =
          slot_data.contains("pilgrimage_allows_roof_access") &&
          slot_data["pilgrimage_allows_roof_access"].get<int>() == 1;
      pilgrimage_allows_paintings =
          slot_data.contains("pilgrimage_allows_paintings") &&
          slot_data["pilgrimage_allows_paintings"].get<int>() == 1;
      sunwarp_access = slot_data.contains("sunwarp_access")
                           ? slot_data["sunwarp_access"].get<SunwarpAccess>()
                           : kSUNWARP_ACCESS_NORMAL;
      sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
                        slot_data["shuffle_sunwarps"].get<int>() == 1;

      if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
        painting_mapping.clear();

        for (const auto& mapping_it :
             slot_data["painting_entrance_to_exit"].items()) {
          painting_mapping[mapping_it.key()] = mapping_it.value();
          painting_codomain.insert(mapping_it.value());
        }
      }

      if (sunwarp_shuffle && slot_data.contains("sunwarp_permutation")) {
        std::vector<int> inverted_sunwarps;
        for (const auto& item : slot_data["sunwarp_permutation"]) {
          inverted_sunwarps.push_back(item);
        }

        for (int i = 0; i < 6; i++) {
          sunwarp_mapping[inverted_sunwarps[i]] = SunwarpMapping{
              .dots = i + 1, .exit_index = inverted_sunwarps[i + 6]};
        }
      }

      std::list<std::string> corrected_keys;
      for (const std::string& key : tracked_data_storage_keys) {
        corrected_keys.push_back(data_storage_prefix + key);
      }

      victory_data_storage_key =
          fmt::format("_read_client_status_{}_{}", apclient->get_team_number(),
                      apclient->get_player_number());

      corrected_keys.push_back(victory_data_storage_key);

      apclient->Get(corrected_keys);
      apclient->SetNotify(corrected_keys);

      ResetReachabilityRequirements();
      RefreshTracker(true);

      {
        std::lock_guard connection_lock(connection_mutex);
        if (!has_connection_result) {
          connected = true;
          has_connection_result = true;
        }
      }
    });

    apclient->set_slot_refused_handler(
        [this, &connection_mutex](const std::list<std::string>& errors) {
          {
            std::lock_guard connection_lock(connection_mutex);
            connected = false;
            has_connection_result = true;
          }

          tracker_frame->SetStatusMessage("Disconnected from Archipelago.");

          std::vector<std::string> error_messages;
          error_messages.push_back("Could not connect to Archipelago.");

          for (const std::string& error : errors) {
            if (error == "InvalidSlot") {
              error_messages.push_back("Invalid player name.");
            } else if (error == "InvalidGame") {
              error_messages.push_back(
                  "The specified player is not playing Lingo.");
            } else if (error == "IncompatibleVersion") {
              error_messages.push_back(
                  "The Archipelago server is not the correct version for this "
                  "client.");
            } else if (error == "InvalidPassword") {
              error_messages.push_back("Incorrect password.");
            } else if (error == "InvalidItemsHandling") {
              error_messages.push_back(
                  "Invalid item handling flag. This is a bug with the tracker. "
                  "Please report it to the lingo-ap-tracker GitHub.");
            } else {
              error_messages.push_back("Unknown error.");
            }
          }

          std::string full_message = hatkirby::implode(error_messages, " ");
          TrackerLog(full_message);

          wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
        });

    client_active = true;

    int timeout = 5000;  // 5 seconds
    int interval = 100;
    int remaining_loops = timeout / interval;
    while (true) {
      {
        std::lock_guard connection_lock(connection_mutex);
        if (has_connection_result) {
          break;
        }
      }

      if (interval == 0) {
        DestroyClient();

        tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
        TrackerLog("Timeout while connecting to Archipelago server.");
        wxMessageBox("Timeout while connecting to Archipelago server.",
                     "Connection failed", wxOK | wxICON_ERROR);

        {
          std::lock_guard connection_lock(connection_mutex);
          connected = false;
          has_connection_result = true;
        }

        break;
      }

      std::this_thread::sleep_for(std::chrono::milliseconds(100));

      interval--;
    }

    if (connected) {
      client_active = false;
    }
  }

  void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
    if (value.is_boolean()) {
      data_storage[key] = value.get<bool>();
      TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
                             (value.get<bool>() ? "true" : "false")));
    } else if (value.is_number()) {
      data_storage[key] = value.get<int>();
      TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
                             value.get<int>()));
    } else if (value.is_object()) {
      if (key.ends_with("PlayerPos")) {
        auto map_value = value.get<std::map<std::string, int>>();
        player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]);
      } else {
        data_storage[key] = value.get<std::map<std::string, int>>();
      }

      TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key));
    } else if (value.is_null()) {
      if (key.ends_with("PlayerPos")) {
        player_pos = std::nullopt;
      } else {
        data_storage.erase(key);
      }

      TrackerLog(fmt::format("Data storage {} retrieved as null", key));
    } else if (value.is_array()) {
      auto list_value = value.get<std::vector<std::string>>();

      if (key.ends_with("Paintings")) {
        data_storage[key] =
            std::set<std::string>(list_value.begin(), list_value.end());
      } else {
        data_storage[key] = list_value;
      }

      TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key,
                             hatkirby::implode(list_value, ", ")));
    }
  }

  bool HasCheckedGameLocation(int location_id) {
    return checked_locations.count(location_id);
  }

  bool HasCheckedHuntPanel(int location_id) {
    std::string key =
        fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
    return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
  }

  bool HasItem(int item_id, int quantity) {
    return inventory.count(item_id) && inventory.at(item_id) >= quantity;
  }

  bool HasAchievement(const std::string& name) {
    std::string key =
        fmt::format("{}Achievement|{}", data_storage_prefix, name);
    return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
  }

  const std::set<std::string>& GetCheckedPaintings() {
    std::string key = fmt::format("{}Paintings", data_storage_prefix);
    if (!data_storage.count(key)) {
      data_storage[key] = std::set<std::string>();
    }

    return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
  }

  bool IsPaintingChecked(const std::string& painting_id) {
    const auto& checked_paintings = GetCheckedPaintings();

    return checked_paintings.count(painting_id) ||
           (painting_mapping.count(painting_id) &&
            checked_paintings.count(painting_mapping.at(painting_id)));
  }

  void RefreshTracker(bool reset) {
    TrackerLog("Refreshing display...");

    RecalculateReachability();

    if (reset) {
      tracker_frame->ResetIndicators();
    } else {
      tracker_frame->UpdateIndicators();
    }
  }

  int64_t GetItemId(const std::string& item_name) {
    int64_t ap_id = apclient->get_item_id(item_name);
    if (ap_id == APClient::INVALID_NAME_ID) {
      TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
    }

    return ap_id;
  }

  std::string GetItemName(int id) { return apclient->get_item_name(id); }

  bool HasReachedGoal() {
    return data_storage.count(victory_data_storage_key) &&
           std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
               30;  // CLIENT_GOAL
  }

  void DestroyClient() {
    client_active = false;
    apclient->reset();
    apclient.reset();
  }
};

APState& GetState() {
  static APState* instance = new APState();
  return *instance;
}

}  // namespace

void AP_SetTrackerFrame(TrackerFrame* arg) { GetState().tracker_frame = arg; }

void AP_Connect(std::string server, std::string player, std::string password) {
  GetState().Connect(server, player, password);
}

bool AP_HasCheckedGameLocation(int location_id) {
  return GetState().HasCheckedGameLocation(location_id);
}

bool AP_HasCheckedHuntPanel(int location_id) {
  return GetState().HasCheckedHuntPanel(location_id);
}

bool AP_HasItem(int item_id, int quantity) {
  return GetState().HasItem(item_id, quantity);
}

std::string AP_GetItemName(int item_id) {
  return GetState().GetItemName(item_id);
}

DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; }

bool AP_AreDoorsGrouped() { return GetState().group_doors; }

bool AP_IsColorShuffle() { return GetState().color_shuffle; }

bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; }

const std::map<std::string, std::string>& AP_GetPaintingMapping() {
  return GetState().painting_mapping;
}

bool AP_IsPaintingMappedTo(const std::string& painting_id) {
  return GetState().painting_codomain.count(painting_id);
}

const std::set<std::string>& AP_GetCheckedPaintings() {
  return GetState().GetCheckedPaintings();
}

bool AP_IsPaintingChecked(const std::string& painting_id) {
  return GetState().IsPaintingChecked(painting_id);
}

int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }

int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }

bool AP_IsLocationVisible(int classification) {
  int world_state = 0;

  switch (GetState().location_checks) {
    case kNORMAL_LOCATIONS:
      world_state = kLOCATION_NORMAL;
      break;
    case kREDUCED_LOCATIONS:
      world_state = kLOCATION_REDUCED;
      break;
    case kPANELSANITY:
      world_state = kLOCATION_INSANITY;
      break;
    default:
      return false;
  }

  if (GetState().door_shuffle_mode && !GetState().early_color_hallways) {
    world_state |= kLOCATION_SMALL_SPHERE_ONE;
  }

  return (world_state & classification);
}

VictoryCondition AP_GetVictoryCondition() {
  return GetState().victory_condition;
}

bool AP_HasAchievement(const std::string& achievement_name) {
  return GetState().HasAchievement(achievement_name);
}

bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; }

bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; }

bool AP_DoesPilgrimageAllowRoofAccess() {
  return GetState().pilgrimage_allows_roof_access;
}

bool AP_DoesPilgrimageAllowPaintings() {
  return GetState().pilgrimage_allows_paintings;
}

SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; }

bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; }

const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() {
  return GetState().sunwarp_mapping;
}

bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }

std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
  return GetState().player_pos;
}