#include "adapters/eft/esp/EspLootScanner.h"

#include <algorithm>
#include <cstring>
#include <string>
#include <utility>
#include <vector>

#include "adapters/eft/esp/EspLootNameResolver.h"
#include "adapters/eft/localization/EftLocalizationService.h"
#include "adapters/eft/world/LootQuery.h"
#include "adapters/il2cpp/Il2CppString.h"
#include "adapters/il2cpp/ManagedRuntime.h"
#include "foundation/math/SpatialMath.h"
#include "adapters/eft/world/WorldAccess.h"
#include "foundation/diagnostics/OnceLogger.h"

namespace ta::adapters::eft::esp
{

namespace
{
    ta::features::esp::EspLootType to_esp_loot_type(const ta::adapters::eft::common::LootKind kind)
    {
        switch (kind)
        {
        case ta::adapters::eft::common::LootKind::Wishlist:
            return ta::features::esp::EspLootType::Wishlist;
        case ta::adapters::eft::common::LootKind::Quest:
            return ta::features::esp::EspLootType::QuestItem;
        case ta::adapters::eft::common::LootKind::Corpse:
            return ta::features::esp::EspLootType::Corpse;
        case ta::adapters::eft::common::LootKind::Container:
            return ta::features::esp::EspLootType::Container;
        case ta::adapters::eft::common::LootKind::Normal:
        default:
            return ta::features::esp::EspLootType::Normal;
        }
    }

    void scan_regular_loot_with_origin(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                       const ta::features::esp::EspSettingsSnapshot& settings,
                                       const ta::features::items::contracts::WishlistSnapshot& wishlist,
                                       const ta::adapters::eft::common::QuestLootIndex* quest_index,
                                       const ta::adapters::unity::vector3& origin,
                                       const float min_dist_sq,
                                       std::vector<ta::features::esp::EspLootEntry>& result)
    {
        if (!settings.normal_items_enabled && !settings.wishlist_items_enabled)
            return;

        const float normal_max_distance = settings.normal_items_enabled ? settings.normal_item_distance : 0.0f;
        const float wishlist_max_distance = settings.wishlist_items_enabled ? settings.wishlist_item_distance : 0.0f;
        const float max_dist = (std::max)(normal_max_distance, wishlist_max_distance);
        const float max_allowed_dist_sq = max_dist * max_dist;
        if (max_allowed_dist_sq <= 0.0f)
            return;

        ta::adapters::eft::common::for_each_loot_item(game_world_instance, [&](Il2CppObject* loot_obj, int)
        {
            if (!loot_obj) return;

            if (ta::adapters::eft::common::LootClassifier::looks_like_corpse(loot_obj) ||
                ta::adapters::eft::common::LootClassifier::looks_like_default_inventory(loot_obj))
                return;

            ta::adapters::unity::vector3 position = {};
            if (!ta::adapters::eft::common::get_component_position(loot_obj, position)) return;

            const float dist_sq_value = ta::foundation::math::distance_sq(origin, position);
            if (dist_sq_value < min_dist_sq || dist_sq_value > max_allowed_dist_sq)
                return;

            if (quest_index && quest_index->objects.contains(loot_obj))
                return;

            Il2CppObject* item_obj = ta::adapters::eft::common::LootClassifier::get_item_object(loot_obj);
            const std::string template_id =
                ta::adapters::eft::common::LootClassifier::get_template_id_from_item(item_obj);

            if (quest_index && quest_index->contains(loot_obj, template_id))
                return;

            const auto classified = ta::adapters::eft::common::LootClassifier::classify_loot_item(
                loot_obj,
                item_obj,
                wishlist,
                template_id);
            if (classified == ta::adapters::eft::common::LootKind::Quest)
                return;

            const ta::features::esp::EspLootType esp_type = to_esp_loot_type(classified);
            const ta::features::esp::EspLootStyle style = ta::features::esp::resolve_esp_loot_style(settings, esp_type);
            if (!style.enabled || dist_sq_value > style.max_distance * style.max_distance)
                return;

            result.push_back({
                EspLootNameResolver::item_short_name(item_obj),
                position,
                esp_type
            });
        });
    }

    void scan_quest_items_with_origin(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                      const ta::features::esp::EspSettingsSnapshot& settings,
                                      const ta::adapters::unity::vector3& origin,
                                      std::vector<ta::features::esp::EspLootEntry>& result)
    {
        const float max_distance_sq = settings.quest_item_distance * settings.quest_item_distance;
        ta::adapters::eft::common::for_each_quest_loot(game_world_instance, [&](Il2CppObject* loot, int)
        {
            if (!loot) return;

            ta::adapters::unity::vector3 position = {};
            if (!ta::adapters::eft::common::get_component_position(loot, position)) return;

            if (ta::foundation::math::distance_sq(origin, position) > max_distance_sq) return;

            Il2CppObject* item = ta::adapters::eft::common::LootClassifier::get_item_object(loot);
            result.push_back({
                EspLootNameResolver::item_short_name(item),
                position,
                ta::features::esp::EspLootType::QuestItem
            });
        });
    }

    bool contains_duplicate_corpse(const std::vector<ta::features::esp::EspLootEntry>& loot,
                                   const ta::adapters::unity::vector3& position)
    {
        return std::ranges::any_of(loot, [&](const ta::features::esp::EspLootEntry& entry)
        {
            if (entry.type != ta::features::esp::EspLootType::Corpse) return false;
            return ta::foundation::math::distance_sq(entry.pos3d, position) < 0.25f;
        });
    }

    bool loot_type_enabled(ta::features::esp::EspLootType type, const ta::features::esp::EspSettingsSnapshot& settings)
    {
        return ta::features::esp::resolve_esp_loot_style(settings, type).enabled;
    }

    bool is_dynamic_loot_type(ta::features::esp::EspLootType type)
    {
        return type != ta::features::esp::EspLootType::Container;
    }

    bool should_keep_existing_loot(const ta::features::esp::EspLootEntry& entry,
                                   const ta::features::esp::EspSettingsSnapshot& settings,
                                   const bool refresh_dynamic,
                                   const bool refresh_static)
    {
        if (!loot_type_enabled(entry.type, settings)) return false;

        const bool dynamic = is_dynamic_loot_type(entry.type);
        if (dynamic && refresh_dynamic) return false;
        if (!dynamic && refresh_static) return false;

        return true;
    }

    std::string log_sample(std::string value)
    {
        for (char& character : value)
        {
            if (character == '\r' || character == '\n' || character == '\t')
                character = ' ';
        }
        constexpr std::size_t k_max_log_sample = 160;
        if (value.size() > k_max_log_sample)
        {
            value.resize(k_max_log_sample);
            value.append("...");
        }
        return value;
    }

    std::string localized_container_name(Il2CppObject* object)
    {
        if (!object || !object->klass)
        {
            ta::foundation::diagnostics::OnceLogger::warn(
                "ESP",
                "container-object-invalid",
                "container.resolve | stage=object status=invalid");
            return {};
        }

        const std::string runtime_name = ta::adapters::unity::get_name(object);
        const std::string class_name = object->klass->name ? object->klass->name : "?";
        ta::foundation::diagnostics::OnceLogger::info(
            "ESP",
            "container-object-sample",
            "container.resolve | stage=object status=available class=" + class_name +
                " runtime_name=" + log_sample(runtime_name));

        const auto template_field = ta::adapters::il2cpp::ManagedRuntime::read_field<Il2CppString*>(
            object, "Template", ta::adapters::il2cpp::LookupPolicy::IncludeBaseClasses);
        if (!template_field)
        {
            ta::foundation::diagnostics::OnceLogger::warn(
                "ESP",
                "container-template-field-unavailable",
                "container.resolve | stage=template path=LootableContainer.Template status=unavailable");
        }
        else if (!template_field.value())
        {
            ta::foundation::diagnostics::OnceLogger::warn(
                "ESP",
                "container-template-field-null",
                "container.resolve | stage=template path=LootableContainer.Template status=null");
        }
        else
        {
            const std::string template_id = ta::adapters::il2cpp::conv_string(template_field.value());
            if (template_id.empty())
            {
                ta::foundation::diagnostics::OnceLogger::warn(
                    "ESP",
                    "container-template-field-empty",
                    "container.resolve | stage=template path=LootableContainer.Template status=empty");
            }
            else
            {
                ta::foundation::diagnostics::OnceLogger::info(
                    "ESP",
                    "container-template-field-selected",
                    "container.resolve | stage=template path=LootableContainer.Template status=selected template_id=" +
                        log_sample(template_id));

                const std::string name_key = template_id + " Name";
                const std::string localized_name =
                    ta::adapters::eft::localization::EftLocalizationService::item_name(template_id);
                if (!localized_name.empty())
                {
                    ta::foundation::diagnostics::OnceLogger::info(
                        "ESP",
                        "container-localization-selected-name",
                        "container.resolve | stage=localization path=LootableContainer.Template key=" +
                            log_sample(name_key) + " status=selected result=" + log_sample(localized_name));
                    return localized_name;
                }

                ta::foundation::diagnostics::OnceLogger::warn(
                    "ESP",
                    "container-localization-name-empty",
                    "container.resolve | stage=localization path=LootableContainer.Template key=" +
                        log_sample(name_key) + " status=empty");

                const std::string short_name_key = template_id + " ShortName";
                const std::string localized_short_name =
                    ta::adapters::eft::localization::EftLocalizationService::item_short_name(template_id);
                if (!localized_short_name.empty())
                {
                    ta::foundation::diagnostics::OnceLogger::info(
                        "ESP",
                        "container-localization-selected-short-name",
                        "container.resolve | stage=localization path=LootableContainer.Template key=" +
                            log_sample(short_name_key) + " status=selected result=" +
                            log_sample(localized_short_name));
                    return localized_short_name;
                }

                ta::foundation::diagnostics::OnceLogger::warn(
                    "ESP",
                    "container-localization-short-name-empty",
                    "container.resolve | stage=localization path=LootableContainer.Template key=" +
                        log_sample(short_name_key) + " status=empty");
            }
        }

        if (!runtime_name.empty())
        {
            ta::foundation::diagnostics::OnceLogger::warn(
                "ESP",
                "container-runtime-name-selected",
                "container.resolve | stage=fallback path=UnityEngine.Object.get_name status=selected result=" +
                    log_sample(runtime_name));
        }
        else
        {
            ta::foundation::diagnostics::OnceLogger::error(
                "ESP",
                "container-all-name-paths-failed",
                "container.resolve | stage=fallback status=failed");
        }
        return runtime_name;
    }

}

void EspLootScanner::refresh_cached_corpses(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance)
{
    cached_corpses_.clear();

    ta::adapters::eft::common::for_each_world_loot_object(game_world_instance, [this](Il2CppObject* obj, int)
    {
        if (!obj) return;
        if (!ta::adapters::eft::common::LootClassifier::looks_like_corpse(obj)) return;

        ta::adapters::unity::vector3 position{};
        if (!ta::adapters::eft::common::get_component_position(obj, position)) return;

        std::string name = ta::adapters::unity::get_name(obj);
        if (name.empty() ||
            ta::adapters::eft::common::LootClassifier::looks_like_default_inventory(obj))
        {
            ta::foundation::diagnostics::OnceLogger::error(
                "ESP", "corpse-name-unavailable",
                "esp.corpse skipped | reason=runtime_name_unavailable");
            return;
        }
        cached_corpses_.push_back({std::move(name), position});
    });
}

void EspLootScanner::refresh_cached_containers(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance)
{
    cached_containers_.clear();

    ta::adapters::eft::common::for_each_world_loot_object(game_world_instance, [this](Il2CppObject* object, int)
    {
        if (!object || !object->klass || !object->klass->name) return;

        if (ta::adapters::eft::common::LootClassifier::looks_like_corpse(object)) return;
        if (!ta::adapters::eft::common::LootClassifier::looks_like_container(object)) return;

        ta::adapters::unity::vector3 position{};
        if (!ta::adapters::eft::common::get_component_position(object, position)) return;

        std::string name = localized_container_name(object);
        if (name.empty())
        {
            ta::foundation::diagnostics::OnceLogger::error(
                "ESP", "container-name-unavailable",
                "Container ESP skipped: no localized or runtime name resolved");
            return;
        }
        cached_containers_.push_back({std::move(name), position});
    });
}

void EspLootScanner::scan_corpses_with_origin(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                               const ta::features::esp::EspSettingsSnapshot& settings,
                                               const ta::adapters::unity::vector3& origin,
                                               const float min_dist_sq,
                                               std::vector<ta::features::esp::EspLootEntry>& result)
{
    refresh_cached_corpses(game_world_instance);

    const float max_dist_sq = settings.corpse_distance * settings.corpse_distance;
    result.reserve(result.size() + cached_corpses_.size());

    for (const auto& entry : cached_corpses_)
    {
        const float dist_sq_value = ta::foundation::math::distance_sq(origin, entry.position);
        if (dist_sq_value < min_dist_sq || dist_sq_value > max_dist_sq) continue;
        if (contains_duplicate_corpse(result, entry.position)) continue;

        result.push_back({entry.name, entry.position, ta::features::esp::EspLootType::Corpse});
    }
}

void EspLootScanner::scan_containers_with_origin(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                                  const ta::features::esp::EspSettingsSnapshot& settings,
                                                  const ta::adapters::unity::vector3& origin,
                                                  std::vector<ta::features::esp::EspLootEntry>& result)
{
    refresh_cached_containers(game_world_instance);

    const float max_distance_sq = settings.container_distance * settings.container_distance;
    result.reserve(result.size() + cached_containers_.size());

    for (const auto& entry : cached_containers_)
    {
        if (ta::foundation::math::distance_sq(origin, entry.position) > max_distance_sq) continue;
        result.push_back({entry.name, entry.position, ta::features::esp::EspLootType::Container});
    }
}

void EspLootScanner::clear_caches() noexcept
{
    cached_world_ = nullptr;
    cached_corpses_.clear();
    cached_containers_.clear();
}

void EspLootScanner::update_loot(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                 const ta::features::esp::EspSettingsSnapshot& settings,
                                 const ta::features::items::contracts::WishlistSnapshot& wishlist,
                                 ta::features::esp::EspWorldSnapshot& snapshot,
                                 const bool refresh_dynamic,
                                 const bool refresh_static)
{
    Il2CppObject* const world = game_world_instance.get_instance();
    if (cached_world_ != world)
    {
        clear_caches();
        cached_world_ = world;
    }

    if (!settings.scan_any_loot())
    {
        snapshot.loot.clear();
        clear_caches();
        return;
    }

    if (!refresh_dynamic && !refresh_static)
    {
        // Keep the existing ESP entries until their refresh bucket is due, but drop entries
        // for options the user has just disabled.
        std::erase_if(snapshot.loot, [&](const ta::features::esp::EspLootEntry& entry)
        {
            return !loot_type_enabled(entry.type, settings);
        });
        return;
    }

    std::vector<ta::features::esp::EspLootEntry> new_loot;
    new_loot.reserve(snapshot.loot.size());

    for (const ta::features::esp::EspLootEntry& entry : snapshot.loot)
    {
        if (should_keep_existing_loot(entry, settings, refresh_dynamic, refresh_static))
            new_loot.push_back(entry);
    }

    const float min_dist_sq = settings.loot_min_distance * settings.loot_min_distance;

    if (refresh_dynamic)
    {
        if (settings.normal_items_enabled || settings.wishlist_items_enabled)
        {
            const ta::adapters::eft::common::QuestLootIndex quest_index = ta::adapters::eft::common::collect_quest_loot_index(game_world_instance);
            scan_regular_loot_with_origin(game_world_instance, settings, wishlist, &quest_index, snapshot.local_position, min_dist_sq, new_loot);
        }

        if (settings.quest_items_enabled)
            scan_quest_items_with_origin(game_world_instance, settings, snapshot.local_position, new_loot);

        if (settings.corpses_enabled)
            scan_corpses_with_origin(game_world_instance, settings, snapshot.local_position, min_dist_sq, new_loot);
        else
        {
            cached_corpses_.clear();
        }
    }

    if (refresh_static)
    {
        if (settings.containers_enabled)
            scan_containers_with_origin(game_world_instance, settings, snapshot.local_position, new_loot);
        else
        {
            cached_containers_.clear();
        }
    }

    snapshot.loot.swap(new_loot);
}
}
