#include "adapters/hooks/game/GameLoopHooks.h"
#include "adapters/hooks/Il2CppMethodHook.h"
#include "application/lifecycle/CallbackBarrier.h"

#include <Windows.h>

#include "adapters/eft/bindings/EftScopeZoomReader.h"
#include "adapters/il2cpp/Il2CppExports.h"
#include "adapters/il2cpp/Il2CppObjectInstance.h"
#include "adapters/il2cpp/Il2CppResolver.h"
#include "adapters/il2cpp/Il2CppObjectAccess.h"
#include "adapters/il2cpp/Il2CppString.h"
#include "application/lifecycle/RaidContext.h"
#include "foundation/diagnostics/Logger.h"
#include "foundation/diagnostics/CrashDiagnostics.h"
#include "foundation/diagnostics/OnceLogger.h"
#include "adapters/hooks/HookCallbacks.h"
#include "application/lifecycle/RuntimeState.h"
#include "adapters/hooks/HookDiagnostics.h"

/// Thin world-level hook callbacks.
namespace ta::adapters::hooks::game_loop
{
    // resolved assemblies & classes
    Il2CppImage* assembly_csharp = nullptr;
    const Il2CppClass* is_in_raid_class = nullptr;

    using update_sig = void(*)(Il2CppObject*);
    update_sig o_tarkov_application_update = nullptr;
    update_sig o_game_world_update = nullptr;

    const Il2CppClass* game_world_class = nullptr;

    bool try_read_in_raid_static(bool& value)
    {
        value = false;

        if (!is_in_raid_class)
            return false;

        FieldInfo* field = nullptr;
        bool read_ok = false;
        bool read_value = false;

#if defined(_MSC_VER)
        __try
        {
            if (!ta::adapters::il2cpp::exports::api().il2cpp_class_get_field_from_name ||
                !ta::adapters::il2cpp::exports::api().il2cpp_field_static_get_value)
            {
                read_ok = false;
            }
            else
            {
                field = ta::adapters::il2cpp::il2cpp_class_get_field_from_name(
                    const_cast<Il2CppClass*>(is_in_raid_class),
                    "InRaid");

                if (field)
                {
                    ta::adapters::il2cpp::il2cpp_field_static_get_value(field, &read_value);
                    read_ok = true;
                }
            }
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {
            read_ok = false;
        }
#else
        if (!ta::adapters::il2cpp::exports::api().il2cpp_class_get_field_from_name ||
            !ta::adapters::il2cpp::exports::api().il2cpp_field_static_get_value)
        {
            return false;
        }

        field = ta::adapters::il2cpp::il2cpp_class_get_field_from_name(
            const_cast<Il2CppClass*>(is_in_raid_class),
            "InRaid");
        if (field)
        {
            ta::adapters::il2cpp::il2cpp_field_static_get_value(field, &read_value);
            read_ok = true;
        }
#endif

        if (!read_ok)
            return false;

        value = read_value;
        return true;
    }

    bool initialized = false;

    // ---- callbacks ----

    void hk_tarkov_application_update(Il2CppObject* instance)
    {
        ta::foundation::diagnostics::crash::CallbackScope diagnostic_scope(
            "hook.application.enter",
            reinterpret_cast<std::uintptr_t>(instance));
        ta::application::runtime::CallbackGuard callback_guard;
        ta::adapters::hooks::diagnostics::mark_tarkov_application_update();
        ta::application::runtime::mark_application_update_thread();

        // Preserve the legacy ordering that is known to survive raid exit:
        // capture InRaid and run module application work before EFT advances
        // its scene lifecycle for this frame.
        if (ta::application::runtime::should_accept_callbacks())
        {
            ta::foundation::diagnostics::crash::set_stage(
                "hook.application.read-in-raid",
                reinterpret_cast<std::uintptr_t>(instance));
            bool raw_in_raid = false;
            if (try_read_in_raid_static(raw_in_raid))
            {
                static bool raid_state_known = false;
                static bool previous_raw_in_raid = false;
                ta::foundation::diagnostics::crash::set_in_raid(raw_in_raid);
                if (!raid_state_known || previous_raw_in_raid != raw_in_raid)
                {
                    ta::foundation::diagnostics::crash::write_event(
                        raw_in_raid ? "hook-raid-enter" : "hook-raid-exit",
                        reinterpret_cast<std::uintptr_t>(instance),
                        previous_raw_in_raid ? 1U : 0U,
                        raw_in_raid ? 1U : 0U);
                    previous_raw_in_raid = raw_in_raid;
                    raid_state_known = true;
                }
                ta::application::raid_context::publish_in_raid_sample(raw_in_raid);
            }

            ta::foundation::diagnostics::crash::set_stage(
                "hook.application.dispatch-runtime",
                reinterpret_cast<std::uintptr_t>(instance));
            ta::adapters::hooks::dispatch_application_update();
        }

        ta::foundation::diagnostics::crash::set_stage(
            "hook.application.original-update",
            reinterpret_cast<std::uintptr_t>(instance),
            reinterpret_cast<std::uintptr_t>(o_tarkov_application_update));
        if (o_tarkov_application_update)
            o_tarkov_application_update(instance);
        ta::foundation::diagnostics::crash::set_stage("hook.application.complete");
    }

    void dispatch_game_world_features(Il2CppObject* instance)
    {
        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.construct-world",
            reinterpret_cast<std::uintptr_t>(instance),
            reinterpret_cast<std::uintptr_t>(game_world_class));
        ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance(instance);
        Il2CppObject* main_player_object = nullptr;
        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.read-main-player",
            reinterpret_cast<std::uintptr_t>(instance));
        if (!game_world_instance.try_get_field("MainPlayer", main_player_object) || !main_player_object)
        {
            ta::foundation::diagnostics::crash::set_stage("hook.gameworld.main-player-unavailable");
            return;
        }

        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.construct-main-player",
            reinterpret_cast<std::uintptr_t>(instance),
            reinterpret_cast<std::uintptr_t>(main_player_object));
        ta::adapters::il2cpp::Il2CppObjectInstance main_player(main_player_object);

        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.read-scope-zoom",
            reinterpret_cast<std::uintptr_t>(main_player_object));
        if (const auto zoom = ta::adapters::eft::bindings::read_scope_zoom(main_player))
            ta::application::raid_context::set_current_zoom(*zoom);

        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.dispatch-runtime",
            reinterpret_cast<std::uintptr_t>(instance),
            reinterpret_cast<std::uintptr_t>(main_player_object),
            reinterpret_cast<std::uintptr_t>(game_world_class));
        ta::adapters::hooks::diagnostics::mark_gameworld_update_dispatch();
        ta::adapters::hooks::dispatch_gameworld_update(game_world_class, game_world_instance, main_player);
        ta::foundation::diagnostics::crash::set_stage("hook.gameworld.runtime-complete");
    }

    bool try_dispatch_game_world_features(Il2CppObject* instance) noexcept
    {
#if defined(_MSC_VER)
        bool completed = true;
        __try
        {
            dispatch_game_world_features(instance);
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {
            completed = false;
        }
        return completed;
#else
        dispatch_game_world_features(instance);
        return true;
#endif
    }

    bool validate_new_raid_player(Il2CppObject* instance) noexcept
    {
        if (!instance || !ta::application::raid_context::new_raid_candidate())
            return false;

#if defined(_MSC_VER)
        bool valid = false;
        __try
        {
            ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance(instance);
            Il2CppObject* main_player = nullptr;
            valid = game_world_instance.try_get_field("MainPlayer", main_player) && main_player;
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {
            valid = false;
        }
        return valid;
#else
        ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance(instance);
        Il2CppObject* main_player = nullptr;
        return game_world_instance.try_get_field("MainPlayer", main_player) && main_player;
#endif
    }

    void hk_game_world_update(Il2CppObject* instance)
    {
        ta::foundation::diagnostics::crash::CallbackScope diagnostic_scope(
            "hook.gameworld.enter",
            reinterpret_cast<std::uintptr_t>(instance),
            reinterpret_cast<std::uintptr_t>(o_game_world_update));
        ta::application::runtime::CallbackGuard callback_guard;
        ta::adapters::hooks::diagnostics::mark_game_world_update();
        // Match the legacy hook: let EFT update the object first. The raid
        // state was captured by TarkovApplication.Update before scene work.
        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.original-update",
            reinterpret_cast<std::uintptr_t>(instance),
            reinterpret_cast<std::uintptr_t>(o_game_world_update));
        if (o_game_world_update)
            o_game_world_update(instance);

        ta::foundation::diagnostics::crash::set_stage(
            "hook.gameworld.post-original-gate",
            reinterpret_cast<std::uintptr_t>(instance),
            ta::application::raid_context::teardown_in_progress() ? 1U : 0U);
        if (!ta::application::runtime::should_accept_callbacks() || !instance)
        {
            ta::foundation::diagnostics::crash::set_stage("hook.gameworld.skipped");
            return;
        }

        if (ta::application::raid_context::teardown_in_progress())
        {
            ta::foundation::diagnostics::crash::set_stage(
                "hook.gameworld.teardown-gate",
                reinterpret_cast<std::uintptr_t>(instance),
                ta::application::raid_context::new_raid_candidate() ? 1U : 0U);
            if (!validate_new_raid_player(instance))
            {
                ta::foundation::diagnostics::crash::set_stage("hook.gameworld.skipped-teardown");
                return;
            }

            ta::application::raid_context::confirm_new_raid();
            ta::foundation::diagnostics::crash::set_in_raid(true);
            ta::foundation::diagnostics::crash::write_event(
                "new-raid-confirmed",
                reinterpret_cast<std::uintptr_t>(instance));
        }

        if (!try_dispatch_game_world_features(instance))
        {
            ta::foundation::diagnostics::crash::write_event(
                "gameworld-custom-dispatch-seh",
                reinterpret_cast<std::uintptr_t>(instance),
                reinterpret_cast<std::uintptr_t>(game_world_class));
            ta::foundation::diagnostics::OnceLogger::error(
                "RUNTIME",
                "gameworld-update-access-violation",
                "GameWorld feature update aborted after an invalid object access");
        }
        ta::foundation::diagnostics::crash::set_stage("hook.gameworld.complete");
    }

    // ---- install ----

    bool install()
    {
        if (initialized) return true;

        // Wait for the game to load Assembly-CSharp.dll before hooking
        while (!ta::adapters::il2cpp::resolve_image("Assembly-CSharp.dll"))
        {
            if (ta::application::runtime::state().is_terminal())
                return false;

            Sleep(50);
        }

        Il2CppImage* assembly = ta::adapters::il2cpp::resolve_image("Assembly-CSharp.dll");
        if (!assembly)
        {
            ta::foundation::logger::warn("World hooks install failed: Assembly-CSharp.dll was not found");
            return false;
        }

        assembly_csharp = assembly;
        is_in_raid_class = ta::adapters::il2cpp::resolve_class(assembly, "EFT", "InGameStatus");
        game_world_class = ta::adapters::il2cpp::resolve_class(assembly, "EFT", "GameWorld");
        const Il2CppClass* tarkov_application_class =
            ta::adapters::il2cpp::resolve_class(assembly, "EFT", "TarkovApplication");

        bool ok = true;

        if (game_world_class)
        {
            ok = ta::adapters::il2cpp::hook_method(game_world_class, "Update", 0,
                                  hk_game_world_update, &o_game_world_update) && ok;
            if (!o_game_world_update)
            {
                ta::foundation::logger::error("Hook failed: EFT.GameWorld.Update");
                ok = false;
            }
        }
        else
        {
            ta::foundation::logger::error("Resolve class failed: EFT.GameWorld");
            ok = false;
        }

        if (tarkov_application_class)
        {
            ok = ta::adapters::il2cpp::hook_method(tarkov_application_class, "Update", 0,
                                  hk_tarkov_application_update, &o_tarkov_application_update) && ok;
            if (!o_tarkov_application_update)
            {
                ta::foundation::logger::error("Hook failed: EFT.TarkovApplication.Update");
                ok = false;
            }
        }
        else
        {
            ta::foundation::logger::error("Resolve class failed: EFT.TarkovApplication");
            ok = false;
        }

        initialized = ok;
        return ok;
    }

    void reset()
    {
        assembly_csharp = nullptr;
        is_in_raid_class = nullptr;
        o_tarkov_application_update = nullptr;
        o_game_world_update = nullptr;
        game_world_class = nullptr;
        initialized = false;
        ta::application::raid_context::reset();
    }
}
